package pipeline

import (
	"context"
	"errors"
	"reflect"
	"strings"
	"testing"

	"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
)

type registryBehaviorCase[M any] struct {
	name             string
	key              string
	stage            ModuleStage
	wrongStage       ModuleStage
	newRegistry      func() any
	register         func(any, string, func() (M, error)) error
	registerWithSpec func(any, ModuleSpec, func() (M, error)) error
	build            func(any, string) (M, error)
	spec             func(any, string) (ModuleSpec, bool)
	registeredKeys   func(any) []string
	nilRegister      func(string, func() (M, error)) error
	nilBuild         func(string) (M, error)
	nilSpec          func(string) (ModuleSpec, bool)
	nilRegisteredKey func() []string
	constructor      func(string) func() (M, error)
	moduleKey        func(M) string
}

func testModuleSpec(key string, stage ModuleStage) ModuleSpec {
	return ModuleSpec{Key: key, Stage: stage, ExecutionClass: contracts.ExecutionClassDeterministic}
}

func TestChunkerRegistryBehavior(t *testing.T) {
	runRegistryBehaviorTests(t, registryBehaviorCase[contracts.Chunker]{
		name:       "ChunkerRegistry",
		key:        "generic-chunker",
		stage:      StageChunk,
		wrongStage: StageExtract,
		newRegistry: func() any {
			return NewChunkerRegistry()
		},
		register: func(registry any, key string, constructor func() (contracts.Chunker, error)) error {
			return registry.(*ChunkerRegistry).RegisterWithSpec(ModuleSpec{
				Key: key, Stage: StageChunk, ExecutionClass: contracts.ExecutionClassDeterministic,
			}, constructor)
		},
		registerWithSpec: func(registry any, spec ModuleSpec, constructor func() (contracts.Chunker, error)) error {
			return registry.(*ChunkerRegistry).RegisterWithSpec(spec, constructor)
		},
		build: func(registry any, key string) (contracts.Chunker, error) {
			return registry.(*ChunkerRegistry).Build(key)
		},
		spec: func(registry any, key string) (ModuleSpec, bool) {
			return registry.(*ChunkerRegistry).Spec(key)
		},
		registeredKeys: func(registry any) []string {
			return registry.(*ChunkerRegistry).RegisteredKeys()
		},
		nilRegister: func(key string, constructor func() (contracts.Chunker, error)) error {
			var registry *ChunkerRegistry
			return registry.RegisterWithSpec(ModuleSpec{
				Key: key, Stage: StageChunk, ExecutionClass: contracts.ExecutionClassDeterministic,
			}, constructor)
		},
		nilBuild: func(key string) (contracts.Chunker, error) {
			var registry *ChunkerRegistry
			return registry.Build(key)
		},
		nilSpec: func(key string) (ModuleSpec, bool) {
			var registry *ChunkerRegistry
			return registry.Spec(key)
		},
		nilRegisteredKey: func() []string {
			var registry *ChunkerRegistry
			return registry.RegisteredKeys()
		},
		constructor: func(key string) func() (contracts.Chunker, error) {
			return func() (contracts.Chunker, error) {
				return registryChunker{key: key}, nil
			}
		},
		moduleKey: func(module contracts.Chunker) string {
			return module.Key()
		},
	})
}

func runRegistryBehaviorTests[M any](t *testing.T, testCase registryBehaviorCase[M]) {
	t.Helper()

	t.Run(testCase.name+"/register and build", func(t *testing.T) {
		registry := testCase.newRegistry()
		if err := testCase.register(registry, testCase.key, testCase.constructor(testCase.key)); err != nil {
			t.Fatalf("Register() error = %v, want nil", err)
		}

		module, err := testCase.build(registry, testCase.key)
		if err != nil {
			t.Fatalf("Build() error = %v, want nil", err)
		}
		if got := testCase.moduleKey(module); got != testCase.key {
			t.Fatalf("module key = %q, want %q", got, testCase.key)
		}
	})

	t.Run(testCase.name+"/metadata registration and lookup", func(t *testing.T) {
		registry := testCase.newRegistry()
		spec := ModuleSpec{
			Key:            " " + testCase.key + " ",
			Stage:          testCase.stage,
			ExecutionClass: contracts.ExecutionClassDeterministic,
			Provides:       []string{" beta ", "alpha", "", "beta"},
			Requires:       []string{" source ", "source", ""},
		}
		if err := testCase.registerWithSpec(registry, spec, testCase.constructor(testCase.key)); err != nil {
			t.Fatalf("RegisterWithSpec() error = %v, want nil", err)
		}

		got, ok := testCase.spec(registry, " "+testCase.key+"\n")
		if !ok {
			t.Fatal("Spec() ok = false, want true")
		}
		want := ModuleSpec{
			Key:            testCase.key,
			Stage:          testCase.stage,
			ExecutionClass: contracts.ExecutionClassDeterministic,
			Provides:       []string{"alpha", "beta"},
			Requires:       []string{"source"},
		}
		if !reflect.DeepEqual(got, want) {
			t.Fatalf("Spec() = %#v, want %#v", got, want)
		}

		got.Provides[0] = "changed"
		again, ok := testCase.spec(registry, testCase.key)
		if !ok {
			t.Fatal("Spec() after caller mutation ok = false, want true")
		}
		if !reflect.DeepEqual(again, want) {
			t.Fatalf("Spec() after caller mutation = %#v, want %#v", again, want)
		}
	})

	t.Run(testCase.name+"/minimal explicit spec registration", func(t *testing.T) {
		registry := testCase.newRegistry()
		if err := testCase.register(registry, " "+testCase.key+" ", testCase.constructor(testCase.key)); err != nil {
			t.Fatalf("Register() error = %v, want nil", err)
		}

		spec, ok := testCase.spec(registry, testCase.key)
		if !ok {
			t.Fatal("Spec() ok = false, want true")
		}
		want := ModuleSpec{Key: testCase.key, Stage: testCase.stage, ExecutionClass: contracts.ExecutionClassDeterministic}
		if !reflect.DeepEqual(spec, want) {
			t.Fatalf("Spec() = %#v, want %#v", spec, want)
		}
	})

	t.Run(testCase.name+"/wrong stage rejection", func(t *testing.T) {
		registry := testCase.newRegistry()
		err := testCase.registerWithSpec(registry, ModuleSpec{Key: testCase.key, Stage: testCase.wrongStage, ExecutionClass: contracts.ExecutionClassDeterministic}, testCase.constructor(testCase.key))
		if err == nil {
			t.Fatal("RegisterWithSpec() error = nil, want error")
		}
		if !strings.Contains(err.Error(), "stage") {
			t.Fatalf("RegisterWithSpec() error = %q, want stage error", err.Error())
		}
	})

	t.Run(testCase.name+"/execution class rejection", func(t *testing.T) {
		for _, spec := range []ModuleSpec{
			{Key: testCase.key, Stage: testCase.stage},
			{Key: testCase.key, Stage: testCase.stage, ExecutionClass: contracts.ExecutionClass("unsupported")},
		} {
			registry := testCase.newRegistry()
			err := testCase.registerWithSpec(registry, spec, testCase.constructor(testCase.key))
			if err == nil {
				t.Fatal("RegisterWithSpec() error = nil, want execution class error")
			}
			if !strings.Contains(err.Error(), "execution class") {
				t.Fatalf("RegisterWithSpec() error = %q, want execution class error", err.Error())
			}
		}
	})

	t.Run(testCase.name+"/key trimming", func(t *testing.T) {
		registry := testCase.newRegistry()
		if err := testCase.register(registry, " "+testCase.key+" ", testCase.constructor(testCase.key)); err != nil {
			t.Fatalf("Register() error = %v, want nil", err)
		}

		module, err := testCase.build(registry, "\t"+testCase.key+"\n")
		if err != nil {
			t.Fatalf("Build() error = %v, want nil", err)
		}
		if got := testCase.moduleKey(module); got != testCase.key {
			t.Fatalf("module key = %q, want %q", got, testCase.key)
		}
	})

	t.Run(testCase.name+"/empty key rejection", func(t *testing.T) {
		registry := testCase.newRegistry()
		err := testCase.register(registry, " \t", testCase.constructor(testCase.key))
		if err == nil {
			t.Fatal("Register() error = nil, want error")
		}
		if !strings.Contains(err.Error(), "key must not be empty") {
			t.Fatalf("Register() error = %q, want empty key error", err.Error())
		}
	})

	t.Run(testCase.name+"/duplicate key rejection", func(t *testing.T) {
		registry := testCase.newRegistry()
		if err := testCase.register(registry, testCase.key, testCase.constructor(testCase.key)); err != nil {
			t.Fatalf("Register() error = %v, want nil", err)
		}

		err := testCase.register(registry, " "+testCase.key+" ", testCase.constructor(testCase.key))
		if err == nil {
			t.Fatal("Register() error = nil, want error")
		}
		if !strings.Contains(err.Error(), "already registered") {
			t.Fatalf("Register() error = %q, want duplicate key error", err.Error())
		}
	})

	t.Run(testCase.name+"/nil constructor rejection", func(t *testing.T) {
		registry := testCase.newRegistry()
		err := testCase.register(registry, testCase.key, nil)
		if err == nil {
			t.Fatal("Register() error = nil, want error")
		}
		if !strings.Contains(err.Error(), "constructor") {
			t.Fatalf("Register() error = %q, want constructor error", err.Error())
		}
	})

	t.Run(testCase.name+"/unknown key build error", func(t *testing.T) {
		registry := testCase.newRegistry()
		_, err := testCase.build(registry, "missing")
		if err == nil {
			t.Fatal("Build() error = nil, want error")
		}
		if !strings.Contains(err.Error(), "not registered") {
			t.Fatalf("Build() error = %q, want unknown key error", err.Error())
		}
	})

	t.Run(testCase.name+"/constructor error wrapping", func(t *testing.T) {
		registry := testCase.newRegistry()
		constructorErr := errors.New("constructor failed")
		if err := testCase.register(registry, testCase.key, func() (M, error) {
			var zero M
			return zero, constructorErr
		}); err != nil {
			t.Fatalf("Register() error = %v, want nil", err)
		}

		_, err := testCase.build(registry, testCase.key)
		if err == nil {
			t.Fatal("Build() error = nil, want error")
		}
		if !errors.Is(err, constructorErr) {
			t.Fatalf("Build() error = %v, want wrapped constructor error", err)
		}
		if !strings.Contains(err.Error(), testCase.key) {
			t.Fatalf("Build() error = %q, want key context", err.Error())
		}
	})

	t.Run(testCase.name+"/nil module rejection", func(t *testing.T) {
		registry := testCase.newRegistry()
		if err := testCase.register(registry, testCase.key, func() (M, error) {
			var zero M
			return zero, nil
		}); err != nil {
			t.Fatalf("Register() error = %v, want nil", err)
		}

		_, err := testCase.build(registry, testCase.key)
		if err == nil {
			t.Fatal("Build() error = nil, want error")
		}
		if !strings.Contains(err.Error(), "returned nil") {
			t.Fatalf("Build() error = %q, want nil module error", err.Error())
		}
	})

	t.Run(testCase.name+"/key mismatch rejection", func(t *testing.T) {
		registry := testCase.newRegistry()
		if err := testCase.register(registry, testCase.key, testCase.constructor("other")); err != nil {
			t.Fatalf("Register() error = %v, want nil", err)
		}

		_, err := testCase.build(registry, testCase.key)
		if err == nil {
			t.Fatal("Build() error = nil, want error")
		}
		if !strings.Contains(err.Error(), "returned") {
			t.Fatalf("Build() error = %q, want mismatch error", err.Error())
		}
	})

	t.Run(testCase.name+"/sorted registered keys", func(t *testing.T) {
		registry := testCase.newRegistry()
		for _, key := range []string{"zeta", "alpha", "middle"} {
			if err := testCase.register(registry, key, testCase.constructor(key)); err != nil {
				t.Fatalf("Register(%q) error = %v, want nil", key, err)
			}
		}

		keys := testCase.registeredKeys(registry)
		want := []string{"alpha", "middle", "zeta"}
		if !reflect.DeepEqual(keys, want) {
			t.Fatalf("RegisteredKeys() = %#v, want %#v", keys, want)
		}

		keys[0] = "changed"
		if got := testCase.registeredKeys(registry); !reflect.DeepEqual(got, want) {
			t.Fatalf("RegisteredKeys() after caller mutation = %#v, want %#v", got, want)
		}
	})

	t.Run(testCase.name+"/nil registry behavior", func(t *testing.T) {
		if err := testCase.nilRegister(testCase.key, testCase.constructor(testCase.key)); err == nil {
			t.Fatal("Register() error = nil, want error")
		}
		if _, err := testCase.nilBuild(testCase.key); err == nil {
			t.Fatal("Build() error = nil, want error")
		}
		if _, ok := testCase.nilSpec(testCase.key); ok {
			t.Fatal("Spec() ok = true, want false")
		}
		if keys := testCase.nilRegisteredKey(); keys != nil {
			t.Fatalf("RegisteredKeys() = %#v, want nil", keys)
		}
	})

	t.Run(testCase.name+"/unknown spec lookup", func(t *testing.T) {
		registry := testCase.newRegistry()
		if _, ok := testCase.spec(registry, "missing"); ok {
			t.Fatal("Spec() ok = true, want false")
		}
	})
}

type registryChunker struct {
	key string
}

func (chunker registryChunker) Key() string {
	return chunker.key
}

func (chunker registryChunker) ReferenceSlots() []contracts.ReferenceSlot {
	return nil
}

func (chunker registryChunker) Plan(ctx context.Context, req contracts.ChunkRequest) (contracts.ChunkPlanResult, error) {
	return contracts.ChunkPlanResult{}, nil
}

type registryOutputEncoder struct {
	key string
}

func (encoder registryOutputEncoder) Key() string {
	return encoder.key
}

func (encoder registryOutputEncoder) Encode(ctx context.Context, req contracts.OutputRequest) (contracts.OutputResult, error) {
	return contracts.OutputResult{}, nil
}
