package streamgate

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

// TestFailureCauseChainCapsAtFour verifies that FailureCauseChain enforces the
// MaxFailureCauses (4) cap on construction and Append, that accessors return
// defensive copies, that distinct codes prove the latest-four retention order,
// and that invalid stage/code/consumer/filter/rule tokens are rejected.
func TestFailureCauseChainCapsAtFour(t *testing.T) {
	// --- Empty chain ---
	empty, err := NewFailureCauseChain(nil)
	if err != nil {
		t.Fatalf("NewFailureCauseChain(nil): %v", err)
	}
	if empty.Len() != 0 {
		t.Errorf("empty chain Len() = %d, want 0", empty.Len())
	}
	if empty.All() != nil {
		t.Errorf("empty chain All() = %v, want nil", empty.All())
	}

	// --- Chain with 4 causes: all retained, distinct codes ---
	codes4 := []string{"code0", "code1", "code2", "code3"}
	causes4 := make([]FailureCause, MaxFailureCauses)
	for i, code := range codes4 {
		c, err := NewFailureCause("stage", code, "", "", "")
		if err != nil {
			t.Fatalf("NewFailureCause: %v", err)
		}
		causes4[i] = c
	}
	chain4, err := NewFailureCauseChain(causes4)
	if err != nil {
		t.Fatalf("NewFailureCauseChain(4): %v", err)
	}
	if chain4.Len() != 4 {
		t.Errorf("4-cause chain Len() = %d, want 4", chain4.Len())
	}
	// Verify order and distinct codes.
	for i := 0; i < 4; i++ {
		c, err := chain4.At(i)
		if err != nil {
			t.Fatalf("At(%d): %v", i, err)
		}
		if c.Code() != codes4[i] {
			t.Errorf("At(%d) Code() = %q, want %q", i, c.Code(), codes4[i])
		}
	}

	// --- Chain with 5 causes: oldest dropped, latest 4 retained in order ---
	codes5 := []string{"code0", "code1", "code2", "code3", "code4"}
	causes5 := make([]FailureCause, MaxFailureCauses+1)
	for i, code := range codes5 {
		c, err := NewFailureCause("stage", code, "", "", "")
		if err != nil {
			t.Fatalf("NewFailureCause: %v", err)
		}
		causes5[i] = c
	}
	chain5, err := NewFailureCauseChain(causes5)
	if err != nil {
		t.Fatalf("NewFailureCauseChain(5): %v", err)
	}
	if chain5.Len() != 4 {
		t.Errorf("5-cause chain Len() = %d, want 4", chain5.Len())
	}
	// Latest 4 are code1..code4 in order.
	for i := 0; i < 4; i++ {
		c, err := chain5.At(i)
		if err != nil {
			t.Fatalf("At(%d): %v", i, err)
		}
		want := codes5[i+1]
		if c.Code() != want {
			t.Errorf("At(%d) Code() = %q, want %q (retention order wrong)", i, c.Code(), want)
		}
	}

	// --- Constructor input mutation does not affect stored chain ---
	t.Run("constructor_input_mutation_isolation", func(t *testing.T) {
		input := make([]FailureCause, MaxFailureCauses+1)
		for i, code := range codes5 {
			c, err := NewFailureCause("stage", code, "", "", "")
			if err != nil {
				t.Fatalf("NewFailureCause: %v", err)
			}
			input[i] = c
		}
		chain, err := NewFailureCauseChain(input)
		if err != nil {
			t.Fatalf("NewFailureCauseChain: %v", err)
		}
		// Replace the element with a different cause.
		mutated, err := NewFailureCause("stage", "mutated", "", "", "")
		if err != nil {
			t.Fatalf("NewFailureCause: %v", err)
		}
		input[0] = mutated
		input[1] = mutated
		input = append(input, mutated)

		// Stored chain should still be code1..code4.
		if chain.Len() != 4 {
			t.Errorf("chain Len() = %d, want 4 (input mutation leaked)", chain.Len())
		}
		for i := 0; i < 4; i++ {
			c, err := chain.At(i)
			if err != nil {
				t.Fatalf("At(%d): %v", i, err)
			}
			want := codes5[i+1]
			if c.Code() != want {
				t.Errorf("At(%d) Code() = %q, want %q (input mutation leaked)", i, c.Code(), want)
			}
		}
	})

	// --- Append to 4-cause chain: oldest dropped, latest 4 retained in order ---
	appendCause, err := NewFailureCause("stage", "code4", "", "", "")
	if err != nil {
		t.Fatalf("NewFailureCause: %v", err)
	}
	appended, err := chain4.Append(appendCause)
	if err != nil {
		t.Fatalf("Append: %v", err)
	}
	if appended.Len() != 4 {
		t.Errorf("appended chain Len() = %d, want 4", appended.Len())
	}
	// Latest 4 are code1..code4 in order.
	for i := 0; i < 4; i++ {
		c, err := appended.At(i)
		if err != nil {
			t.Fatalf("At(%d): %v", i, err)
		}
		want := codes5[i+1]
		if c.Code() != want {
			t.Errorf("At(%d) Code() = %q, want %q (Append retention order wrong)", i, c.Code(), want)
		}
	}

	// --- Append returned chain mutation does not affect original ---
	t.Run("append_returned_chain_mutation_isolation", func(t *testing.T) {
		appendCause2, err := NewFailureCause("stage", "code4", "", "", "")
		if err != nil {
			t.Fatalf("NewFailureCause: %v", err)
		}
		appended2, err := chain4.Append(appendCause2)
		if err != nil {
			t.Fatalf("Append: %v", err)
		}
		// Replace retained elements in the returned chain directly.
		mutated, err := NewFailureCause("stage", "mutated", "", "", "")
		if err != nil {
			t.Fatalf("NewFailureCause: %v", err)
		}
		appended2.causes[0] = mutated
		appended2.causes = append(appended2.causes, mutated)

		// Original chain4 should be unchanged.
		if chain4.Len() != 4 {
			t.Errorf("chain4 Len() = %d, want 4 (Append mutation leaked)", chain4.Len())
		}
		for i := 0; i < 4; i++ {
			c, err := chain4.At(i)
			if err != nil {
				t.Fatalf("At(%d): %v", i, err)
			}
			if c.Code() != codes4[i] {
				t.Errorf("At(%d) Code() = %q, want %q (Append mutation leaked)", i, c.Code(), codes4[i])
			}
		}
	})

	// --- Append to 3-cause chain: 4 retained ---
	chain3, err := NewFailureCauseChain(causes4[:3])
	if err != nil {
		t.Fatalf("NewFailureCauseChain(3): %v", err)
	}
	appended3, err := chain3.Append(appendCause)
	if err != nil {
		t.Fatalf("Append: %v", err)
	}
	if appended3.Len() != 4 {
		t.Errorf("appended 3-cause chain Len() = %d, want 4", appended3.Len())
	}

	// --- At() returns defensive copy ---
	orig, err := chain4.At(0)
	if err != nil {
		t.Fatalf("At(0): %v", err)
	}
	if orig.Stage() != "stage" {
		t.Errorf("At(0) Stage() = %q, want %q", orig.Stage(), "stage")
	}

	// --- At() out of range returns error ---
	if _, err := chain4.At(-1); err == nil {
		t.Error("At(-1) should return error")
	}
	if _, err := chain4.At(4); err == nil {
		t.Error("At(4) should return error")
	}

	// --- All() returns defensive copy ---
	all := chain4.All()
	if len(all) != 4 {
		t.Errorf("All() length = %d, want 4", len(all))
	}
	all = append(all, orig)
	if chain4.Len() != 4 {
		t.Errorf("chain4 Len() after All() mutation = %d, want 4", chain4.Len())
	}

	// --- All() element replacement does not affect stored ---
	all2 := chain4.All()
	mutated, err := NewFailureCause("stage", "mutated", "", "", "")
	if err != nil {
		t.Fatalf("NewFailureCause: %v", err)
	}
	all2[0] = mutated
	c, err := chain4.At(0)
	if err != nil {
		t.Fatalf("At(0): %v", err)
	}
	if c.Code() != codes4[0] {
		t.Errorf("chain4 At(0) Code() = %q, want %q (All mutation leaked)", c.Code(), codes4[0])
	}

	// --- Copy() returns defensive copy ---
	cp := chain4.Copy()
	if cp.Len() != 4 {
		t.Errorf("Copy() Len() = %d, want 4", cp.Len())
	}
	cpCauses := cp.All()
	cpCauses = append(cpCauses, orig)
	if chain4.Len() != 4 {
		t.Errorf("chain4 Len() after Copy() mutation = %d, want 4", chain4.Len())
	}

	// --- Copy() element replacement does not affect original ---
	cp2 := chain4.Copy()
	cp2.causes[0] = mutated
	c, err = chain4.At(0)
	if err != nil {
		t.Fatalf("At(0): %v", err)
	}
	if c.Code() != codes4[0] {
		t.Errorf("chain4 At(0) Code() = %q, want %q (Copy mutation leaked)", c.Code(), codes4[0])
	}

	// --- Invalid cause in chain is rejected by constructor ---
	invalidCause := FailureCause{
		stage: StableToken{value: "INVALID"},
		code:  mustToken("code"),
	}
	if _, err := NewFailureCauseChain([]FailureCause{invalidCause}); err == nil {
		t.Error("NewFailureCauseChain with invalid cause should return error")
	}

	// --- Append rejects invalid cause ---
	if _, err := chain4.Append(invalidCause); err == nil {
		t.Error("Append with invalid cause should return error")
	}

	// --- Invalid token table ---
	t.Run("invalid_stage_empty", func(t *testing.T) {
		_, err := NewFailureCause("", "code", "", "", "")
		if err == nil {
			t.Fatal("empty stage should be rejected")
		}
	})
	t.Run("invalid_stage_uppercase", func(t *testing.T) {
		_, err := NewFailureCause("STAGE", "code", "", "", "")
		if err == nil {
			t.Fatal("uppercase stage should be rejected")
		}
	})
	t.Run("invalid_code_empty", func(t *testing.T) {
		_, err := NewFailureCause("stage", "", "", "", "")
		if err == nil {
			t.Fatal("empty code should be rejected")
		}
	})
	t.Run("invalid_code_uppercase", func(t *testing.T) {
		_, err := NewFailureCause("stage", "CODE", "", "", "")
		if err == nil {
			t.Fatal("uppercase code should be rejected")
		}
	})
	t.Run("invalid_consumer_uppercase", func(t *testing.T) {
		_, err := NewFailureCause("stage", "code", "CONSUMER", "", "")
		if err == nil {
			t.Fatal("uppercase consumer should be rejected")
		}
	})
	t.Run("invalid_filter_uppercase", func(t *testing.T) {
		_, err := NewFailureCause("stage", "code", "", "FILTER", "")
		if err == nil {
			t.Fatal("uppercase filter should be rejected")
		}
	})
	t.Run("invalid_rule_id_uppercase", func(t *testing.T) {
		_, err := NewFailureCause("stage", "code", "", "", "RULE")
		if err == nil {
			t.Fatal("uppercase rule id should be rejected")
		}
	})
}

// TestTerminalResultValidation verifies that TerminalResult enforces the
// success/error mutual exclusion, required channel/timestamp, required
// external descriptor for error results, cause cap, and that accessors return
// defensive copies.
func TestTerminalResultValidation(t *testing.T) {
	ts := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
	desc, err := NewExternalDescriptor("error.type", "code", "message", "")
	if err != nil {
		t.Fatalf("NewExternalDescriptor: %v", err)
	}

	// --- Success terminal: valid ---
	success, err := NewSuccessTerminalResult("ch", ts)
	if err != nil {
		t.Fatalf("NewSuccessTerminalResult: %v", err)
	}
	if !success.Success() {
		t.Error("success terminal should be success")
	}
	if success.Error() {
		t.Error("success terminal should not be error")
	}
	if success.ExternalDesc() != nil {
		t.Error("success terminal should not have external descriptor")
	}
	if success.FailureCauses().Len() != 0 {
		t.Errorf("success terminal FailureCauses().Len() = %d, want 0", success.FailureCauses().Len())
	}
	if err := success.Validate(); err != nil {
		t.Errorf("success terminal Validate() = %v, want nil", err)
	}

	// --- Error terminal with descriptor: valid ---
	errorResult, err := NewErrorTerminalResult("ch", desc, FailureCauseChain{}, ts)
	if err != nil {
		t.Fatalf("NewErrorTerminalResult: %v", err)
	}
	if errorResult.Success() {
		t.Error("error terminal should not be success")
	}
	if !errorResult.Error() {
		t.Error("error terminal should be error")
	}
	if errorResult.ExternalDesc() == nil {
		t.Error("error terminal should have external descriptor")
	}
	if err := errorResult.Validate(); err != nil {
		t.Errorf("error terminal Validate() = %v, want nil", err)
	}

	// --- Error terminal with descriptor + 4 causes: valid ---
	codes4 := []string{"code0", "code1", "code2", "code3"}
	causes4 := make([]FailureCause, MaxFailureCauses)
	for i, code := range codes4 {
		c, err := NewFailureCause("stage", code, "", "", "")
		if err != nil {
			t.Fatalf("NewFailureCause: %v", err)
		}
		causes4[i] = c
	}
	chain4, err := NewFailureCauseChain(causes4)
	if err != nil {
		t.Fatalf("NewFailureCauseChain: %v", err)
	}
	errorWithCauses, err := NewErrorTerminalResult("ch", desc, chain4, ts)
	if err != nil {
		t.Fatalf("NewErrorTerminalResult with causes: %v", err)
	}
	if err := errorWithCauses.Validate(); err != nil {
		t.Errorf("error terminal with 4 causes Validate() = %v, want nil", err)
	}

	// --- Error terminal without descriptor: rejected ---
	if _, err := NewErrorTerminalResult("ch", ExternalDescriptor{}, FailureCauseChain{}, ts); err == nil {
		t.Fatal("error terminal without descriptor should return error")
	}

	// --- Error terminal with nil descriptor: rejected ---
	badErr := TerminalResult{
		channel:    "ch",
		err:        true,
		occurredAt: ts,
	}
	if err := badErr.Validate(); err == nil {
		t.Fatal("error terminal with nil descriptor should fail Validate")
	}

	// --- Success terminal with descriptor: rejected ---
	badSuccess := TerminalResult{
		channel:      "ch",
		success:      true,
		externalDesc: &desc,
		occurredAt:   ts,
	}
	if err := badSuccess.Validate(); err == nil {
		t.Fatal("success terminal with descriptor should fail Validate")
	}

	// --- Success terminal with causes: rejected ---
	badSuccessCauses := TerminalResult{
		channel:       "ch",
		success:       true,
		failureCauses: chain4,
		occurredAt:    ts,
	}
	if err := badSuccessCauses.Validate(); err == nil {
		t.Fatal("success terminal with causes should fail Validate")
	}

	// --- Neither success nor error: rejected ---
	neither := TerminalResult{
		channel:    "ch",
		occurredAt: ts,
	}
	if err := neither.Validate(); err == nil {
		t.Fatal("terminal with neither success nor error should fail Validate")
	}

	// --- Both success and error: rejected ---
	both := TerminalResult{
		channel:      "ch",
		success:      true,
		err:          true,
		externalDesc: &desc,
		occurredAt:   ts,
	}
	if err := both.Validate(); err == nil {
		t.Fatal("terminal with both success and error should fail Validate")
	}

	// --- Empty channel: rejected ---
	if _, err := NewSuccessTerminalResult("", ts); err == nil {
		t.Fatal("success terminal with empty channel should return error")
	}
	if _, err := NewErrorTerminalResult("", desc, FailureCauseChain{}, ts); err == nil {
		t.Fatal("error terminal with empty channel should return error")
	}

	// --- Zero timestamp: rejected ---
	if _, err := NewSuccessTerminalResult("ch", time.Time{}); err == nil {
		t.Fatal("success terminal with zero timestamp should return error")
	}
	if _, err := NewErrorTerminalResult("ch", desc, FailureCauseChain{}, time.Time{}); err == nil {
		t.Fatal("error terminal with zero timestamp should return error")
	}

	// --- Error with 5 causes: rejected ---
	causes5 := make([]FailureCause, MaxFailureCauses+1)
	for i := range causes5 {
		c, err := NewFailureCause("stage", "code", "", "", "")
		if err != nil {
			t.Fatalf("NewFailureCause: %v", err)
		}
		causes5[i] = c
	}
	chain5Raw := FailureCauseChain{causes: causes5}
	badTr := TerminalResult{
		channel:       "ch",
		err:           true,
		externalDesc:  &desc,
		failureCauses: chain5Raw,
		occurredAt:    ts,
	}
	if err := badTr.Validate(); err == nil {
		t.Fatal("error terminal with 5 causes should fail Validate")
	}

	// --- ExternalDesc accessor returns defensive copy ---
	ed := errorResult.ExternalDesc()
	if ed == nil {
		t.Fatal("ExternalDesc() returned nil")
	}
	// Mutate the returned descriptor.
	*ed = ExternalDescriptor{}
	// The stored descriptor should be unchanged.
	ed2 := errorResult.ExternalDesc()
	if ed2 == nil {
		t.Fatal("ExternalDesc() returned nil after mutation")
	}
	if ed2.Type() != "error.type" {
		t.Errorf("ExternalDesc() Type() = %q, want %q (mutation leaked)", ed2.Type(), "error.type")
	}

	// --- FailureCauses accessor returns defensive copy ---
	fc := errorWithCauses.FailureCauses()
	if fc.Len() != 4 {
		t.Errorf("FailureCauses().Len() = %d, want 4", fc.Len())
	}
	fcCauses := fc.All()
	fcCauses = append(fcCauses, fcCauses[0])
	if errorWithCauses.FailureCauses().Len() != 4 {
		t.Error("FailureCauses() mutated (accessor mutation leaked)")
	}

	// --- FailureCauses accessor element mutation does not affect stored ---
	fc2 := errorWithCauses.FailureCauses()
	mutated, err := NewFailureCause("stage", "mutated", "", "", "")
	if err != nil {
		t.Fatalf("NewFailureCause: %v", err)
	}
	fc2.causes[0] = mutated
	fc3 := errorWithCauses.FailureCauses()
	if fc3.Len() != 4 {
		t.Errorf("FailureCauses().Len() after mutation = %d, want 4", fc3.Len())
	}
	c, err := fc3.At(0)
	if err != nil {
		t.Fatalf("At(0): %v", err)
	}
	if c.Code() != codes4[0] {
		t.Errorf("FailureCauses().At(0) Code() = %q, want %q (accessor mutation leaked)", c.Code(), codes4[0])
	}

	// --- FailureCause invalid token table ---
	t.Run("invalid_stage_empty", func(t *testing.T) {
		_, err := NewFailureCause("", "code", "", "", "")
		if err == nil {
			t.Fatal("empty stage should be rejected")
		}
	})
	t.Run("invalid_stage_uppercase", func(t *testing.T) {
		_, err := NewFailureCause("STAGE", "code", "", "", "")
		if err == nil {
			t.Fatal("uppercase stage should be rejected")
		}
	})
	t.Run("invalid_code_empty", func(t *testing.T) {
		_, err := NewFailureCause("stage", "", "", "", "")
		if err == nil {
			t.Fatal("empty code should be rejected")
		}
	})
	t.Run("invalid_code_uppercase", func(t *testing.T) {
		_, err := NewFailureCause("stage", "CODE", "", "", "")
		if err == nil {
			t.Fatal("uppercase code should be rejected")
		}
	})
	t.Run("invalid_consumer_uppercase", func(t *testing.T) {
		_, err := NewFailureCause("stage", "code", "CONSUMER", "", "")
		if err == nil {
			t.Fatal("uppercase consumer should be rejected")
		}
	})
	t.Run("invalid_filter_uppercase", func(t *testing.T) {
		_, err := NewFailureCause("stage", "code", "", "FILTER", "")
		if err == nil {
			t.Fatal("uppercase filter should be rejected")
		}
	})
	t.Run("invalid_rule_id_uppercase", func(t *testing.T) {
		_, err := NewFailureCause("stage", "code", "", "", "RULE")
		if err == nil {
			t.Fatal("uppercase rule id should be rejected")
		}
	})
}

// fakeReleaseSink is a test-only implementation of ReleaseSink that records
// all committed payloads. It is used solely for the compile-time interface
// assertion; single-terminal behavior is verified in the host fixture.
type fakeReleaseSink struct {
	responseStarts []ResponseStart
	releases       []ReleaseEvent
	terminals      []TerminalResult
}

// Compile-time assertion that fakeReleaseSink implements ReleaseSink.
var _ ReleaseSink = (*fakeReleaseSink)(nil)

func (f *fakeReleaseSink) CommitResponseStart(ctx context.Context, rs ResponseStart) (CommitState, error) {
	f.responseStarts = append(f.responseStarts, rs)
	return CommitStateStreamOpen, nil
}

func (f *fakeReleaseSink) Release(ctx context.Context, ev ReleaseEvent) (CommitState, error) {
	f.releases = append(f.releases, ev)
	return CommitStateStreamOpen, nil
}

func (f *fakeReleaseSink) CommitTerminal(ctx context.Context, tr TerminalResult) (CommitState, error) {
	f.terminals = append(f.terminals, tr)
	return CommitStateTerminalCommitted, nil
}

// TestReleaseSinkContractCompiles verifies that the ReleaseSink interface
// contract is satisfied by a test-only fake sink via a compile-time assertion,
// and that all three methods can be invoked through the interface.
func TestReleaseSinkContractCompiles(t *testing.T) {
	ctx := context.Background()
	sink := &fakeReleaseSink{}

	// Verify the interface is usable through the interface type.
	var iface ReleaseSink = sink

	// CommitResponseStart
	rs, err := NewResponseStart("ch", 200, map[string]string{"x-custom": "val"}, time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC))
	if err != nil {
		t.Fatalf("NewResponseStart: %v", err)
	}
	state, err := iface.CommitResponseStart(ctx, rs)
	if err != nil {
		t.Fatalf("CommitResponseStart: %v", err)
	}
	if state != CommitStateStreamOpen {
		t.Errorf("CommitResponseStart state = %q, want %q", state, CommitStateStreamOpen)
	}
	if len(sink.responseStarts) != 1 {
		t.Errorf("responseStarts length = %d, want 1", len(sink.responseStarts))
	}

	// Release
	releaseEv, err := NewReleaseTextDeltaEvent("ch", "hello", time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC))
	if err != nil {
		t.Fatalf("NewReleaseTextDeltaEvent: %v", err)
	}
	state, err = iface.Release(ctx, releaseEv)
	if err != nil {
		t.Fatalf("Release: %v", err)
	}
	if state != CommitStateStreamOpen {
		t.Errorf("Release state = %q, want %q", state, CommitStateStreamOpen)
	}
	if len(sink.releases) != 1 {
		t.Errorf("releases length = %d, want 1", len(sink.releases))
	}

	// CommitTerminal
	terminal, err := NewSuccessTerminalResult("ch", time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC))
	if err != nil {
		t.Fatalf("NewSuccessTerminalResult: %v", err)
	}
	state, err = iface.CommitTerminal(ctx, terminal)
	if err != nil {
		t.Fatalf("CommitTerminal: %v", err)
	}
	if state != CommitStateTerminalCommitted {
		t.Errorf("CommitTerminal state = %q, want %q", state, CommitStateTerminalCommitted)
	}
	if len(sink.terminals) != 1 {
		t.Errorf("terminals length = %d, want 1", len(sink.terminals))
	}
}
