// Copyright © 2024 Ory Corp
// SPDX-License-Identifier: Apache-2.0

package testhelpers

import (
	"context"
	"net/http"
	"net/http/httptest"

	"github.com/gofrs/uuid"

	"github.com/ory/kratos/embedx"
	"github.com/ory/x/configx"
	"github.com/ory/x/contextx"
)

type (
	TestConfigProvider struct {
		contextx.Contextualizer
		Options []configx.OptionModifier
	}
	contextKey int
)

func (t *TestConfigProvider) NewProvider(ctx context.Context, opts ...configx.OptionModifier) (*configx.Provider, error) {
	return configx.New(ctx, []byte(embedx.ConfigSchema), append(t.Options, opts...)...)
}

func (t *TestConfigProvider) Config(ctx context.Context, config *configx.Provider) *configx.Provider {
	config = t.Contextualizer.Config(ctx, config)
	values, ok := ctx.Value(contextConfigKey).([]map[string]any)
	if !ok {
		return config
	}

	opts := make([]configx.OptionModifier, 0, len(values))
	opts = append(opts, configx.WithValues(config.All()))
	for _, v := range values {
		opts = append(opts, configx.WithValues(v))
	}
	config, err := t.NewProvider(ctx, opts...)
	if err != nil {
		// This is not production code. The provider is only used in tests.
		panic(err)
	}
	return config
}

const contextConfigKey contextKey = 1

var (
	_ contextx.Contextualizer = (*TestConfigProvider)(nil)
)

func WithConfigValue(ctx context.Context, key string, value any) context.Context {
	return WithConfigValues(ctx, map[string]any{key: value})
}

func WithConfigValues(ctx context.Context, setValues ...map[string]any) context.Context {
	values, ok := ctx.Value(contextConfigKey).([]map[string]any)
	if !ok {
		values = make([]map[string]any, 0)
	}
	newValues := make([]map[string]any, len(values), len(values)+len(setValues))
	copy(newValues, values)
	newValues = append(newValues, setValues...)

	return context.WithValue(ctx, contextConfigKey, newValues)
}

type ConfigurableTestHandler struct {
	configs map[uuid.UUID][]map[string]any
	handler http.Handler
}

func NewConfigurableTestHandler(h http.Handler) *ConfigurableTestHandler {
	return &ConfigurableTestHandler{
		configs: make(map[uuid.UUID][]map[string]any),
		handler: h,
	}
}

func (t *ConfigurableTestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	cID := r.Header.Get("Test-Config-Id")
	if config, ok := t.configs[uuid.FromStringOrNil(cID)]; ok {
		r = r.WithContext(WithConfigValues(r.Context(), config...))
	}
	t.handler.ServeHTTP(w, r)
}

func (t *ConfigurableTestHandler) RegisterConfig(config ...map[string]any) uuid.UUID {
	id := uuid.Must(uuid.NewV4())
	t.configs[id] = config
	return id
}

func (t *ConfigurableTestHandler) UseConfig(r *http.Request, id uuid.UUID) *http.Request {
	r.Header.Set("Test-Config-Id", id.String())
	return r
}

func (t *ConfigurableTestHandler) UseConfigValues(r *http.Request, values ...map[string]any) *http.Request {
	return t.UseConfig(r, t.RegisterConfig(values...))
}

type ConfigurableTestServer struct {
	*httptest.Server
	handler   *ConfigurableTestHandler
	transport http.RoundTripper
}

func NewConfigurableTestServer(h http.Handler) *ConfigurableTestServer {
	handler := NewConfigurableTestHandler(h)
	server := httptest.NewServer(handler)

	t := server.Client().Transport
	cts := &ConfigurableTestServer{
		handler:   handler,
		Server:    server,
		transport: t,
	}
	server.Client().Transport = cts
	return cts
}

func (t *ConfigurableTestServer) RoundTrip(r *http.Request) (*http.Response, error) {
	config, ok := r.Context().Value(contextConfigKey).([]map[string]any)
	if ok && config != nil {
		r = t.handler.UseConfigValues(r, config...)
	}
	return t.transport.RoundTrip(r)
}

type AutoContextClient struct {
	*http.Client
	transport http.RoundTripper
	ctx       context.Context
}

func (t *ConfigurableTestServer) Client(ctx context.Context) *AutoContextClient {
	baseClient := *t.Server.Client()
	autoClient := &AutoContextClient{
		Client:    &baseClient,
		transport: t,
		ctx:       ctx,
	}
	baseClient.Transport = autoClient
	return autoClient
}

func (c *AutoContextClient) RoundTrip(r *http.Request) (*http.Response, error) {
	return c.transport.RoundTrip(r.WithContext(c.ctx))
}
