package streamgate

import (
	"context"
	"errors"
	"testing"
	"time"
)

func TestNoopFilterContract(t *testing.T) {
	t.Run("DefaultConstructor", func(t *testing.T) {
		filter, err := NewNoopFilter()
		if err != nil {
			t.Fatalf("NewNoopFilter failed: %v", err)
		}
		if filter.ID() != DefaultNoopFilterID {
			t.Errorf("ID() = %q, want %q", filter.ID(), DefaultNoopFilterID)
		}

		builder := NewFilterContextBuilder("gen-1", "att-1")
		fctx, err := builder.Build()
		if err != nil {
			t.Fatalf("Build FilterContext failed: %v", err)
		}

		if !filter.Applies(fctx) {
			t.Errorf("Applies() = false, want true")
		}

		holdReq := filter.HoldRequirement(fctx)
		if holdReq.Mode() != FilterHoldModeNone {
			t.Errorf("HoldRequirement.Mode() = %v, want %v", holdReq.Mode(), FilterHoldModeNone)
		}
		if holdReq.Channel() != DefaultNoopChannel {
			t.Errorf("HoldRequirement.Channel() = %q, want %q", holdReq.Channel(), DefaultNoopChannel)
		}
	})

	t.Run("CustomChannelConstructor", func(t *testing.T) {
		filter, err := NewNoopFilterWithChannel("custom_noop", "custom_ch")
		if err != nil {
			t.Fatalf("NewNoopFilterWithChannel failed: %v", err)
		}
		if filter.ID() != "custom_noop" {
			t.Errorf("ID() = %q, want custom_noop", filter.ID())
		}
		builder := NewFilterContextBuilder("gen-1", "att-1")
		fctx, _ := builder.Build()
		holdReq := filter.HoldRequirement(fctx)
		if holdReq.Channel() != "custom_ch" {
			t.Errorf("HoldRequirement.Channel() = %q, want custom_ch", holdReq.Channel())
		}
	})

	t.Run("EvaluatePassDecision", func(t *testing.T) {
		filter, _ := NewNoopFilter()
		builder := NewFilterContextBuilder("gen-1", "att-1")
		fctx, _ := builder.Build()

		ev, err := NewTextDeltaEvent("ch", "hello", time.Now())
		if err != nil {
			t.Fatalf("NewTextDeltaEvent failed: %v", err)
		}
		batch, err := NewEvidenceBatch(
			[]NormalizedEvent{ev},
			nil, nil, nil, false,
			CommitStateTransportUncommitted,
			time.Now(),
		)
		if err != nil {
			t.Fatalf("NewEvidenceBatch failed: %v", err)
		}

		ctx := context.Background()
		decision, err := filter.Evaluate(ctx, fctx, batch)
		if err != nil {
			t.Fatalf("Evaluate failed: %v", err)
		}

		if decision.Kind() != FilterDecisionKindPass {
			t.Errorf("Decision Kind = %v, want %v", decision.Kind(), FilterDecisionKindPass)
		}
		if decision.FilterID() != DefaultNoopFilterID {
			t.Errorf("Decision FilterID = %q, want %q", decision.FilterID(), DefaultNoopFilterID)
		}
		if decision.ConsumerID() != DefaultNoopConsumerID {
			t.Errorf("Decision ConsumerID = %q, want %q", decision.ConsumerID(), DefaultNoopConsumerID)
		}
		if decision.RuleID() != DefaultNoopRuleID {
			t.Errorf("Decision RuleID = %q, want %q", decision.RuleID(), DefaultNoopRuleID)
		}
		if decision.Intent() != nil {
			t.Errorf("Decision Intent must be nil for pass decision")
		}
	})

	t.Run("EvaluateContextCancelled", func(t *testing.T) {
		filter, _ := NewNoopFilter()
		builder := NewFilterContextBuilder("gen-1", "att-1")
		fctx, _ := builder.Build()
		ev, _ := NewTextDeltaEvent("ch", "hello", time.Now())
		batch, _ := NewEvidenceBatch(
			[]NormalizedEvent{ev},
			nil, nil, nil, false,
			CommitStateTransportUncommitted,
			time.Now(),
		)

		ctx, cancel := context.WithCancel(context.Background())
		cancel()

		_, err := filter.Evaluate(ctx, fctx, batch)
		if err == nil {
			t.Errorf("Evaluate with cancelled context expected error, got nil")
		}
	})
}

func TestRuntimeOptionsValidation(t *testing.T) {
	t.Run("DefaultOptionsValid", func(t *testing.T) {
		opts := DefaultRuntimeOptions()
		if err := opts.Validate(); err != nil {
			t.Fatalf("DefaultRuntimeOptions validation failed: %v", err)
		}
		if opts.MaxEvidenceRunes() != DefaultMaxEvidenceRunes {
			t.Errorf("MaxEvidenceRunes = %d, want %d", opts.MaxEvidenceRunes(), DefaultMaxEvidenceRunes)
		}
		if opts.MaxBufferRunes() != DefaultMaxBufferRunes {
			t.Errorf("MaxBufferRunes = %d, want %d", opts.MaxBufferRunes(), DefaultMaxBufferRunes)
		}
		if opts.MaxIngressSnapshotBytes() != DefaultMaxIngressSnapshotBytes {
			t.Errorf("MaxIngressSnapshotBytes = %d, want %d", opts.MaxIngressSnapshotBytes(), DefaultMaxIngressSnapshotBytes)
		}
		if opts.MaxRecoveryAttemptsTotal() != DefaultMaxRecoveryAttemptsTotal {
			t.Errorf("MaxRecoveryAttemptsTotal = %d, want %d", opts.MaxRecoveryAttemptsTotal(), DefaultMaxRecoveryAttemptsTotal)
		}
	})

	t.Run("InvalidRunes", func(t *testing.T) {
		_, err := NewRuntimeOptions(0, 4096, 1024, 3, GateCoordinatorOptions{}, RecoveryCoordinatorOptions{})
		if err == nil {
			t.Errorf("Expected error for 0 max evidence runes")
		}

		_, err = NewRuntimeOptions(500, 100, 1024, 3, GateCoordinatorOptions{}, RecoveryCoordinatorOptions{})
		if err == nil {
			t.Errorf("Expected error for maxBuffer < maxEvidence")
		}
	})

	t.Run("InvalidIngressBytes", func(t *testing.T) {
		_, err := NewRuntimeOptions(500, 4096, 0, 3, GateCoordinatorOptions{}, RecoveryCoordinatorOptions{})
		if err == nil {
			t.Errorf("Expected error for 0 max ingress bytes")
		}
	})

	t.Run("IngressAbsoluteCapBoundaries", func(t *testing.T) {
		limit := int64(DefaultMaxIngressSnapshotBytes)

		// limit-1 should succeed
		_, err := NewRuntimeOptions(500, 4096, limit-1, 3, GateCoordinatorOptions{}, RecoveryCoordinatorOptions{})
		if err != nil {
			t.Errorf("NewRuntimeOptions(limit-1) unexpected error: %v", err)
		}

		// limit should succeed
		_, err = NewRuntimeOptions(500, 4096, limit, 3, GateCoordinatorOptions{}, RecoveryCoordinatorOptions{})
		if err != nil {
			t.Errorf("NewRuntimeOptions(limit) unexpected error: %v", err)
		}

		// limit+1 should fail
		_, err = NewRuntimeOptions(500, 4096, limit+1, 3, GateCoordinatorOptions{}, RecoveryCoordinatorOptions{})
		if err == nil {
			t.Errorf("Expected error for max ingress bytes > 16 MiB")
		}
	})

	t.Run("DirectValidateRejectsIngressLimitPlusOne", func(t *testing.T) {
		opts := DefaultRuntimeOptions()
		opts.maxIngressSnapshotBytes = DefaultMaxIngressSnapshotBytes + 1
		if err := opts.Validate(); err == nil {
			t.Errorf("Direct Validate() with limit+1 bytes should fail")
		}
	})

	t.Run("InvalidRecoveryAttempts", func(t *testing.T) {
		_, err := NewRuntimeOptions(500, 4096, 1024, 4, GateCoordinatorOptions{}, RecoveryCoordinatorOptions{})
		if err == nil {
			t.Errorf("Expected error for max recovery attempts > 3")
		}
		_, err = NewRuntimeOptions(500, 4096, 1024, -1, GateCoordinatorOptions{}, RecoveryCoordinatorOptions{})
		if err == nil {
			t.Errorf("Expected error for negative max recovery attempts")
		}
	})
}

type fakeDispatcher struct{}

func (fakeDispatcher) DispatchAttempt(ctx context.Context, request RebuiltRequest) (AttemptBinding, error) {
	return AttemptBinding{}, errors.New("fake dispatch")
}

type fakeRebuilder struct{}

func (fakeRebuilder) RebuildRequest(ctx context.Context, snapshot RecoveryRequestSnapshotRef, plan RecoveryPlan) (RebuiltRequestDraft, error) {
	return RebuiltRequestDraft{}, errors.New("fake rebuild")
}

type fakeSink struct{}

func (fakeSink) CommitResponseStart(ctx context.Context, rs ResponseStart) (CommitState, error) {
	return CommitStateTransportUncommitted, nil
}

func (fakeSink) Release(ctx context.Context, ev ReleaseEvent) (CommitState, error) {
	return CommitStateStreamOpen, nil
}

func (fakeSink) CommitTerminal(ctx context.Context, tr TerminalResult) (CommitState, error) {
	return CommitStateTerminalCommitted, nil
}

type mockPreparer struct{}

func (mockPreparer) PrepareRecoveryPlan(ctx context.Context, plan RecoveryPlan, snapshot RecoveryPreparationSnapshot) (RecoveryDirective, error) {
	return NewRecoveryDirectiveExact("req-ref-1")
}

type mockPrepFactory struct{}

func (mockPrepFactory) CreatePreparationSnapshot(ctx context.Context, snapshot RequestRuntimeSnapshot, binding AttemptBinding) (RecoveryPreparationSnapshot, error) {
	return nil, nil
}

func TestRequestRuntimeSnapshotContract(t *testing.T) {
	regSnap, err := NewFilterRegistrySnapshot("gen-1", nil, nil)
	if err != nil {
		t.Fatalf("NewFilterRegistrySnapshot failed: %v", err)
	}

	snapRef, err := NewRecoveryRequestSnapshotRef("snap.ref.1", 100, 200, 1024)
	if err != nil {
		t.Fatalf("NewRecoveryRequestSnapshotRef failed: %v", err)
	}

	opts := DefaultRuntimeOptions()
	disp := fakeDispatcher{}
	rebuilder := fakeRebuilder{}
	sink := fakeSink{}

	t.Run("ValidConstruction", func(t *testing.T) {
		reqSnap, err := NewRequestRuntimeSnapshot(
			"req-123",
			"gen-1",
			"prod",
			"v1/chat/completions",
			"gpt4",
			opts,
			regSnap,
			nil,
			snapRef,
			disp,
			rebuilder,
			nil,
			nil,
			sink,
		)
		if err != nil {
			t.Fatalf("NewRequestRuntimeSnapshot failed: %v", err)
		}

		if reqSnap.RequestID() != "req-123" {
			t.Errorf("RequestID() = %q, want req-123", reqSnap.RequestID())
		}
		if reqSnap.ConfigGeneration() != "gen-1" {
			t.Errorf("ConfigGeneration() = %q, want gen-1", reqSnap.ConfigGeneration())
		}
		if reqSnap.Environment() != "prod" {
			t.Errorf("Environment() = %q, want prod", reqSnap.Environment())
		}
		if reqSnap.Endpoint() != "v1/chat/completions" {
			t.Errorf("Endpoint() = %q, want v1/chat/completions", reqSnap.Endpoint())
		}
		if reqSnap.Family() != "gpt4" {
			t.Errorf("Family() = %q, want gpt4", reqSnap.Family())
		}
		if reqSnap.Dispatcher() == nil || reqSnap.Rebuilder() == nil || reqSnap.Sink() == nil {
			t.Errorf("Seam accessors returned nil")
		}
		if reqSnap.Preparer() != nil {
			t.Errorf("Preparer() expected nil")
		}
		if reqSnap.PreparationSnapshotFactory() != nil {
			t.Errorf("PreparationSnapshotFactory() expected nil")
		}

		// Test BuildAttemptFilterContext
		target, err := NewAttemptTarget("standard", "gpt-4-turbo", "openai", "primary", nil)
		if err != nil {
			t.Fatalf("NewAttemptTarget failed: %v", err)
		}
		fctx, err := reqSnap.BuildAttemptFilterContext("att-1", target, 1, CommitStateTransportUncommitted)
		if err != nil {
			t.Fatalf("BuildAttemptFilterContext failed: %v", err)
		}
		if fctx.AttemptID() != "att-1" {
			t.Errorf("FilterContext AttemptID() = %q, want att-1", fctx.AttemptID())
		}
		if fctx.EpochID() != 1 {
			t.Errorf("FilterContext EpochID() = %d, want 1", fctx.EpochID())
		}
		if fctx.StableCorrelation() != "req-123" {
			t.Errorf("FilterContext StableCorrelation() = %q, want req-123", fctx.StableCorrelation())
		}
		if fctx.ActualModel() != "gpt-4-turbo" {
			t.Errorf("FilterContext ActualModel() = %q, want gpt-4-turbo", fctx.ActualModel())
		}
	})

	t.Run("PreparerFactoryPairingValidation", func(t *testing.T) {
		// Both non-nil should succeed
		p := mockPreparer{}
		f := mockPrepFactory{}
		_, err := NewRequestRuntimeSnapshot(
			"req-123", "gen-1", "prod", "v1/chat/completions", "gpt4",
			opts, regSnap, nil, snapRef, disp, rebuilder, p, f, sink,
		)
		if err != nil {
			t.Errorf("Both non-nil preparer/factory failed: %v", err)
		}

		// Preparer non-nil, factory nil should fail
		_, err = NewRequestRuntimeSnapshot(
			"req-123", "gen-1", "prod", "v1/chat/completions", "gpt4",
			opts, regSnap, nil, snapRef, disp, rebuilder, p, nil, sink,
		)
		if err == nil {
			t.Errorf("Expected error for preparer set without factory")
		}

		// Preparer nil, factory non-nil should fail
		_, err = NewRequestRuntimeSnapshot(
			"req-123", "gen-1", "prod", "v1/chat/completions", "gpt4",
			opts, regSnap, nil, snapRef, disp, rebuilder, nil, f, sink,
		)
		if err == nil {
			t.Errorf("Expected error for factory set without preparer")
		}
	})

	t.Run("GenerationMismatch", func(t *testing.T) {
		// matching generation should succeed
		_, err := NewRequestRuntimeSnapshot(
			"req-123",
			"gen-1",
			"prod",
			"v1/chat/completions",
			"gpt4",
			opts,
			regSnap,
			nil,
			snapRef,
			disp,
			rebuilder,
			nil,
			nil,
			sink,
		)
		if err != nil {
			t.Fatalf("NewRequestRuntimeSnapshot with matching generation failed: %v", err)
		}

		// mismatching generation should fail
		_, err = NewRequestRuntimeSnapshot(
			"req-123",
			"gen-2",
			"prod",
			"v1/chat/completions",
			"gpt4",
			opts,
			regSnap,
			nil,
			snapRef,
			disp,
			rebuilder,
			nil,
			nil,
			sink,
		)
		if err == nil {
			t.Errorf("Expected error for config/registry generation mismatch")
		}
	})

	t.Run("DirectValidateRejectsGenerationMismatch", func(t *testing.T) {
		reqSnap, err := NewRequestRuntimeSnapshot(
			"req-123",
			"gen-1",
			"prod",
			"v1/chat/completions",
			"gpt4",
			opts,
			regSnap,
			nil,
			snapRef,
			disp,
			rebuilder,
			nil,
			nil,
			sink,
		)
		if err != nil {
			t.Fatalf("NewRequestRuntimeSnapshot failed: %v", err)
		}

		// Tamper configGen to cause mismatch
		reqSnap.configGen = StableToken{value: "gen-999"}
		if err := reqSnap.Validate(); err == nil {
			t.Errorf("Direct Validate() with mismatched generation should fail")
		}
	})

	t.Run("ValidationErrors", func(t *testing.T) {
		_, err := NewRequestRuntimeSnapshot(
			"", "gen-1", "prod", "ep", "fam", opts, regSnap, nil, snapRef, disp, rebuilder, nil, nil, sink,
		)
		if err == nil {
			t.Errorf("Expected error for empty requestID")
		}

		_, err = NewRequestRuntimeSnapshot(
			"req-1", "gen-1", "prod", "ep", "fam", opts, regSnap, nil, snapRef, nil, rebuilder, nil, nil, sink,
		)
		if err == nil {
			t.Errorf("Expected error for nil dispatcher")
		}

		_, err = NewRequestRuntimeSnapshot(
			"req-1", "gen-1", "prod", "ep", "fam", opts, regSnap, nil, snapRef, disp, nil, nil, nil, sink,
		)
		if err == nil {
			t.Errorf("Expected error for nil rebuilder")
		}

		_, err = NewRequestRuntimeSnapshot(
			"req-1", "gen-1", "prod", "ep", "fam", opts, regSnap, nil, snapRef, disp, rebuilder, nil, nil, nil,
		)
		if err == nil {
			t.Errorf("Expected error for nil sink")
		}
	})
}

func TestRuntimeOptionsRecoveryStrategyLimits(t *testing.T) {
	base, err := NewRuntimeOptions(500, 4096, 1024, 3, GateCoordinatorOptions{}, RecoveryCoordinatorOptions{})
	if err != nil {
		t.Fatalf("NewRuntimeOptions: %v", err)
	}

	t.Run("omitted inherits total", func(t *testing.T) {
		if got := base.MaxRecoveryAttemptsByStrategy(); got != nil {
			t.Errorf("expected nil strategy map for omitted caps, got %v", got)
		}
	})

	t.Run("explicit caps are snapshot and defensively copied", func(t *testing.T) {
		limits := map[RecoveryStrategy]int{
			RecoveryStrategyExactReplay:        1,
			RecoveryStrategyContinuationRepair: 0,
			RecoveryStrategySchemaRepair:       2,
		}
		opts, err := base.WithRecoveryStrategyLimits(limits)
		if err != nil {
			t.Fatalf("WithRecoveryStrategyLimits: %v", err)
		}
		limits[RecoveryStrategyExactReplay] = 99 // mutate caller map after snapshot
		got := opts.MaxRecoveryAttemptsByStrategy()
		if got[RecoveryStrategyExactReplay] != 1 || got[RecoveryStrategyContinuationRepair] != 0 || got[RecoveryStrategySchemaRepair] != 2 {
			t.Errorf("strategy caps not snapshot defensively: %v", got)
		}
		got[RecoveryStrategyExactReplay] = 42 // mutate returned copy
		if again := opts.MaxRecoveryAttemptsByStrategy(); again[RecoveryStrategyExactReplay] != 1 {
			t.Errorf("accessor did not return a defensive copy: %v", again)
		}
		// The wired snapshot must build a valid recovery policy honoring the caps.
		if _, err := NewRecoveryPolicySnapshot(opts.MaxRecoveryAttemptsTotal(), opts.MaxRecoveryAttemptsByStrategy()); err != nil {
			t.Errorf("NewRecoveryPolicySnapshot from options: %v", err)
		}
	})

	t.Run("cap above total rejected", func(t *testing.T) {
		if _, err := base.WithRecoveryStrategyLimits(map[RecoveryStrategy]int{RecoveryStrategyExactReplay: 4}); err == nil {
			t.Errorf("expected error for strategy cap above total")
		}
	})

	t.Run("negative cap rejected", func(t *testing.T) {
		if _, err := base.WithRecoveryStrategyLimits(map[RecoveryStrategy]int{RecoveryStrategyExactReplay: -1}); err == nil {
			t.Errorf("expected error for negative strategy cap")
		}
	})

	t.Run("non-fault strategy rejected", func(t *testing.T) {
		if _, err := base.WithRecoveryStrategyLimits(map[RecoveryStrategy]int{RecoveryStrategyManagedContinuation: 1}); err == nil {
			t.Errorf("expected error for non-fault strategy cap")
		}
	})
}
