package pipeline

import (
	"reflect"
	"strings"
	"testing"

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

func TestValidateModuleSpecRequiresSupportedExecutionClass(t *testing.T) {
	for _, test := range []struct {
		name  string
		class contracts.ExecutionClass
		want  string
	}{
		{name: "missing", want: "execution class"},
		{name: "unsupported", class: "remote", want: "unsupported"},
		{name: "deterministic", class: contracts.ExecutionClassDeterministic},
		{name: "llm backed", class: contracts.ExecutionClassLLMBacked},
	} {
		t.Run(test.name, func(t *testing.T) {
			spec := normalizeModuleSpec(ModuleSpec{Key: "module", Stage: StageChunk, ExecutionClass: test.class})
			err := validateModuleSpec("chunker", StageChunk, spec)
			if test.want == "" && err != nil {
				t.Fatalf("validateModuleSpec() error = %v, want nil", err)
			}
			if test.want != "" && (err == nil || !strings.Contains(err.Error(), test.want)) {
				t.Fatalf("validateModuleSpec() error = %v, want %q", err, test.want)
			}
		})
	}
}

func TestCloneModuleSpecPreservesCorrectionProtocol(t *testing.T) {
	spec := normalizeModuleSpec(ModuleSpec{
		Key:                " module ",
		Stage:              StageChunk,
		ExecutionClass:     contracts.ExecutionClassLLMBacked,
		CorrectionProtocol: " single_response_v1 ",
	})
	if spec.CorrectionProtocol != contracts.CorrectionProtocolSingleResponseV1 {
		t.Fatalf("normalized CorrectionProtocol = %q, want %q", spec.CorrectionProtocol, contracts.CorrectionProtocolSingleResponseV1)
	}
	cloned := cloneModuleSpec(spec)
	if !reflect.DeepEqual(cloned, spec) {
		t.Fatalf("cloneModuleSpec() = %#v, want %#v", cloned, spec)
	}
}

func TestValidateModuleSpecCorrectionProtocolEligibility(t *testing.T) {
	tests := []struct {
		name     string
		stage    ModuleStage
		class    contracts.ExecutionClass
		protocol contracts.CorrectionProtocol
		want     string
	}{
		{name: "LLM chunk", stage: StageChunk, class: contracts.ExecutionClassLLMBacked, protocol: contracts.CorrectionProtocolSingleResponseV1},
		{name: "LLM extract", stage: StageExtract, class: contracts.ExecutionClassLLMBacked, protocol: contracts.CorrectionProtocolSingleResponseV1},
		{name: "LLM merge", stage: StageMerge, class: contracts.ExecutionClassLLMBacked, protocol: contracts.CorrectionProtocolSingleResponseV1},
		{name: "LLM normalize", stage: StageNormalize, class: contracts.ExecutionClassLLMBacked, protocol: contracts.CorrectionProtocolSingleResponseV1},
		{name: "deterministic chunk", stage: StageChunk, class: contracts.ExecutionClassDeterministic, protocol: contracts.CorrectionProtocolSingleResponseV1, want: "LLM-backed"},
		{name: "input", stage: StageInput, class: contracts.ExecutionClassLLMBacked, protocol: contracts.CorrectionProtocolSingleResponseV1, want: "not supported"},
		{name: "output", stage: StageOutput, class: contracts.ExecutionClassLLMBacked, protocol: contracts.CorrectionProtocolSingleResponseV1, want: "not supported"},
		{name: "unknown protocol", stage: StageChunk, class: contracts.ExecutionClassLLMBacked, protocol: "unsupported", want: "unsupported correction protocol"},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			spec := normalizeModuleSpec(ModuleSpec{
				Key:                "module",
				Stage:              test.stage,
				ExecutionClass:     test.class,
				CorrectionProtocol: test.protocol,
			})
			err := validateModuleSpec("module", test.stage, spec)
			if test.want == "" && err != nil {
				t.Fatalf("validateModuleSpec() error = %v, want nil", err)
			}
			if test.want != "" && (err == nil || !strings.Contains(err.Error(), test.want)) {
				t.Fatalf("validateModuleSpec() error = %v, want %q", err, test.want)
			}
		})
	}
}

func TestNormalizeValidatorSpecRejectsCorrectionProtocol(t *testing.T) {
	_, err := normalizeValidatorSpec(ValidatorSpec{
		Key:                "validator",
		ExecutionClass:     contracts.ExecutionClassLLMBacked,
		CorrectionProtocol: contracts.CorrectionProtocolSingleResponseV1,
	})
	if err == nil || !strings.Contains(err.Error(), "correction protocol") {
		t.Fatalf("normalizeValidatorSpec() error = %v, want correction protocol error", err)
	}
}

func TestValidateModuleSpecAllowsReferenceSlotsForEligibleStages(t *testing.T) {
	tests := []struct {
		name  string
		kind  string
		stage ModuleStage
	}{
		{name: "chunker", kind: "chunker", stage: StageChunk},
		{name: "extractor", kind: "extractor", stage: StageExtract},
		{name: "merger", kind: "merger", stage: StageMerge},
		{name: "normalizer", kind: "normalizer", stage: StageNormalize},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			spec := normalizeModuleSpec(ModuleSpec{
				Key:            "module",
				Stage:          test.stage,
				ExecutionClass: contracts.ExecutionClassDeterministic,
				ReferenceSlots: []contracts.ReferenceSlot{
					{Name: "roster", Description: "Character roster", MaxBytes: 1024},
				},
			})
			err := validateModuleSpec(test.kind, test.stage, spec)
			if err != nil {
				t.Fatalf("validateModuleSpec() error = %v, want nil", err)
			}
		})
	}
}

func TestValidateModuleSpecRejectsReferenceSlotsForIneligibleStages(t *testing.T) {
	tests := []struct {
		name  string
		kind  string
		stage ModuleStage
	}{
		{name: "input", kind: "input adapter", stage: StageInput},
		{name: "validate", kind: "validator", stage: StageValidate},
		{name: "output", kind: "output encoder", stage: StageOutput},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			spec := normalizeModuleSpec(ModuleSpec{
				Key:            "module",
				Stage:          test.stage,
				ExecutionClass: contracts.ExecutionClassDeterministic,
				ReferenceSlots: []contracts.ReferenceSlot{
					{Name: "roster"},
				},
			})
			err := validateModuleSpec(test.kind, test.stage, spec)
			if err == nil {
				t.Fatal("validateModuleSpec() error = nil, want error")
			}
			if !strings.Contains(err.Error(), "reference slots") {
				t.Fatalf("validateModuleSpec() error = %q, want reference slots context", err.Error())
			}
		})
	}
}

func TestValidateModuleSpecRejectsInvalidReferenceSlotsForEligibleStages(t *testing.T) {
	invalidSlots := []struct {
		name  string
		slots []contracts.ReferenceSlot
		want  string
	}{
		{
			name:  "empty name",
			slots: []contracts.ReferenceSlot{{Name: " "}},
			want:  "name",
		},
		{
			name: "duplicate name after trim",
			slots: []contracts.ReferenceSlot{
				{Name: "roster"},
				{Name: " roster "},
			},
			want: "duplicated",
		},
		{
			name:  "negative max bytes",
			slots: []contracts.ReferenceSlot{{Name: "roster", MaxBytes: -1}},
			want:  "max_bytes",
		},
	}
	eligibleStages := []struct {
		name  string
		kind  string
		stage ModuleStage
	}{
		{name: "chunk", kind: "chunker", stage: StageChunk},
		{name: "extract", kind: "extractor", stage: StageExtract},
		{name: "merge", kind: "merger", stage: StageMerge},
		{name: "normalize", kind: "normalizer", stage: StageNormalize},
	}

	for _, stage := range eligibleStages {
		for _, invalid := range invalidSlots {
			t.Run(stage.name+"/"+invalid.name, func(t *testing.T) {
				spec := normalizeModuleSpec(ModuleSpec{
					Key:            "module",
					Stage:          stage.stage,
					ExecutionClass: contracts.ExecutionClassDeterministic,
					ReferenceSlots: invalid.slots,
				})
				err := validateModuleSpec(stage.kind, stage.stage, spec)
				if err == nil {
					t.Fatal("validateModuleSpec() error = nil, want error")
				}
				if !strings.Contains(err.Error(), invalid.want) {
					t.Fatalf("validateModuleSpec() error = %q, want %q", err.Error(), invalid.want)
				}
			})
		}
	}
}
