package httptest

import (
	"crypto/tls"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/kataras/iris/v12"
	"github.com/kataras/iris/v12/context"
	"github.com/kataras/iris/v12/core/router"
	"github.com/kataras/iris/v12/i18n"

	"github.com/iris-contrib/httpexpect/v2"
)

type (
	// OptionSetter sets a configuration field to the configuration
	OptionSetter interface {
		// Set receives a pointer to the Configuration type and does the job of filling it
		Set(c *Configuration)
	}
	// OptionSet implements the OptionSetter
	OptionSet func(c *Configuration)
)

// Set is the func which makes the OptionSet an OptionSetter, this is used mostly
func (o OptionSet) Set(c *Configuration) {
	o(c)
}

// Configuration httptest configuration
type Configuration struct {
	// URL the base url.
	// Defaults to empty string "".
	URL string
	// Debug if true then debug messages from the httpexpect will be shown when a test runs
	// Defaults to false.
	Debug bool
	// LogLevel sets the application's log level.
	// Defaults to "disable" when testing.
	LogLevel string

	// If true then the underline httpexpect report will be acquired by the NewRequireReporter
	// call instead of the default NewAssertReporter.
	// Defaults to false.
	Strict bool // Note: if more reports are available in the future then add a Reporter interface as a field.
}

// Set implements the OptionSetter for the Configuration itself
func (c Configuration) Set(main *Configuration) {
	main.URL = c.URL
	main.Debug = c.Debug
	if c.LogLevel != "" {
		main.LogLevel = c.LogLevel
	}
	main.Strict = c.Strict
}

var (
	// URL if set then it sets the httptest's BaseURL.
	// Defaults to empty string "".
	URL = func(schemeAndHost string) OptionSet {
		return func(c *Configuration) {
			c.URL = schemeAndHost
		}
	}
	// Debug if true then debug messages from the httpexpect will be shown when a test runs
	// Defaults to false.
	Debug = func(val bool) OptionSet {
		return func(c *Configuration) {
			c.Debug = val
		}
	}

	// LogLevel sets the application's log level.
	// Defaults to disabled when testing.
	LogLevel = func(level string) OptionSet {
		return func(c *Configuration) {
			c.LogLevel = level
		}
	}

	// Strict sets the Strict configuration field to "val".
	// Applies the NewRequireReporter instead of the default one.
	// Use this if you want the test to fail on first error, before all checks have been done.
	Strict = func(val bool) OptionSet {
		return func(c *Configuration) {
			c.Strict = val
		}
	}
)

// DefaultConfiguration returns the default configuration for the httptest.
func DefaultConfiguration() *Configuration {
	return &Configuration{URL: "", Debug: false, LogLevel: "disable"}
}

// New Prepares and returns a new test framework based on the "app".
// Usage:
//
//	httptest.New(t, app)
//
// With options:
//
//	httptest.New(t, app, httptest.URL(...), httptest.Debug(true), httptest.LogLevel("debug"), httptest.Strict(true))
//
// Example at: https://github.com/kataras/iris/tree/master/_examples/testing/httptest.
func New(t *testing.T, app *iris.Application, setters ...OptionSetter) *httpexpect.Expect {
	conf := DefaultConfiguration()
	for _, setter := range setters {
		setter.Set(conf)
	}

	// set the logger or disable it (default).
	app.Logger().SetLevel(conf.LogLevel)

	if err := app.Build(); err != nil {
		if conf.LogLevel != "disable" {
			app.Logger().Println(err.Error())
			return nil
		}
	}

	var reporter httpexpect.Reporter

	if conf.Strict {
		reporter = httpexpect.NewRequireReporter(t)
	} else {
		reporter = httpexpect.NewAssertReporter(t)
	}

	testConfiguration := httpexpect.Config{
		BaseURL: conf.URL,
		Client: &http.Client{
			Transport: httpexpect.NewBinder(app),
			Jar:       httpexpect.NewJar(),
		},
		Reporter: reporter,
	}

	if conf.Debug {
		testConfiguration.Printers = []httpexpect.Printer{
			httpexpect.NewDebugPrinter(t, true),
		}
	}

	return httpexpect.WithConfig(testConfiguration)
}

// NewInsecure same as New but receives a single host instead of the whole framework.
// Useful for testing running TLS servers.
func NewInsecure(t *testing.T, setters ...OptionSetter) *httpexpect.Expect {
	conf := DefaultConfiguration()
	for _, setter := range setters {
		setter.Set(conf)
	}
	transport := &http.Transport{
		TLSClientConfig: &tls.Config{InsecureSkipVerify: true, MinVersion: tls.VersionTLS13}, // lint:ignore
	}

	testConfiguration := httpexpect.Config{
		BaseURL: conf.URL,
		Client: &http.Client{
			Transport: transport,
			Jar:       httpexpect.NewJar(),
		},
		Reporter: httpexpect.NewAssertReporter(t),
	}

	if conf.Debug {
		testConfiguration.Printers = []httpexpect.Printer{
			httpexpect.NewDebugPrinter(t, true),
		}
	}

	return httpexpect.WithConfig(testConfiguration)
}

// Aliases for "net/http/httptest" package. See `Do` package-level function.
var (
	NewRecorder = httptest.NewRecorder
	NewRequest  = httptest.NewRequest
)

// Do is a simple helper which can be used to test handlers individually
// with the "net/http/httptest" package.
// This package contains aliases for `NewRequest` and `NewRecorder` too.
//
// For a more efficient testing please use the `New` function instead.
func Do(w http.ResponseWriter, r *http.Request, handler iris.Handler, irisConfigurators ...iris.Configurator) {
	app := new(iris.Application)
	app.I18n = i18n.New()
	app.Configure(iris.WithConfiguration(iris.DefaultConfiguration()), iris.WithLogLevel("disable"))
	app.Configure(irisConfigurators...)

	app.HTTPErrorHandler = router.NewDefaultHandler(app.ConfigurationReadOnly(), app.Logger())
	app.ContextPool = context.New(func() interface{} {
		return context.NewContext(app)
	})

	ctx := app.ContextPool.Acquire(w, r)
	handler(ctx)
	app.ContextPool.Release(ctx)
}
