package streamgate

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

// testNow is a deterministic timestamp used in tests.
var testNow = time.Date(2026, 7, 24, 12, 0, 0, 0, time.UTC)

// testSink is a configurable test-only ReleaseSink that records calls and
// can be programmed to fail specific calls.
type testSink struct {
	mu                 sync.Mutex
	responseStarts     []ResponseStart
	releases           []ReleaseEvent
	terminals          []TerminalResult
	state              CommitState
	failAfterSuccesses int  // >0: fail after N successes; 0: no failure
	failAll            bool // if true, all releases fail
	successCount       int  // how many successful releases have occurred (only tracked when failAfterSuccesses > 0)
}

func (s *testSink) CommitResponseStart(ctx context.Context, rs ResponseStart) (CommitState, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.responseStarts = append(s.responseStarts, rs)
	s.state = CommitStateStreamOpen
	return CommitStateStreamOpen, nil
}

func (s *testSink) Release(ctx context.Context, ev ReleaseEvent) (CommitState, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	if s.failAll {
		return "", errors.New("test: forced release failure")
	}
	if s.failAfterSuccesses > 0 {
		s.successCount++
		if s.successCount > s.failAfterSuccesses {
			return "", errors.New("test: forced release failure")
		}
	}
	s.releases = append(s.releases, ev)
	return s.state, nil
}

func (s *testSink) CommitTerminal(ctx context.Context, tr TerminalResult) (CommitState, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.terminals = append(s.terminals, tr)
	s.state = CommitStateTerminalCommitted
	return CommitStateTerminalCommitted, nil
}

// ---------------------------------------------------------------------------
// REVIEW_API-1: Request-scope staging and monotonic sink state
// ---------------------------------------------------------------------------

// TestCommitBoundaryStagesUntilFirstSafeRelease verifies that stage calls
// produce zero sink invocations and the first release commits start→event
// order with a single start.
func TestCommitBoundaryStagesUntilFirstSafeRelease(t *testing.T) {
	ctx := context.Background()
	sink := &testSink{}
	b, err := NewCommitBoundary(sink)
	if err != nil {
		t.Fatalf("NewCommitBoundary: %v", err)
	}

	rs, _ := NewResponseStart("ch", 200, map[string]string{"x-test": "v"}, testNow)

	if err := b.BeginAttempt("a1"); err != nil {
		t.Fatalf("BeginAttempt: %v", err)
	}

	if err := b.StageResponseStart("a1", rs); err != nil {
		t.Fatalf("StageResponseStart: %v", err)
	}

	if got := b.State(); got != CommitStateTransportUncommitted {
		t.Errorf("state before release = %q, want %q", got, CommitStateTransportUncommitted)
	}

	ev1, _ := NewReleaseTextDeltaEvent("ch", "hello ", testNow)
	ev2, _ := NewReleaseTextDeltaEvent("ch", "world", testNow)

	prog, err := b.ReleaseSafe(ctx, "a1", []ReleaseEvent{ev1, ev2})
	if err != nil {
		t.Fatalf("ReleaseSafe: %v", err)
	}

	if len(sink.responseStarts) != 1 {
		t.Errorf("responseStarts = %d, want 1", len(sink.responseStarts))
	}
	if len(sink.releases) != 2 {
		t.Errorf("releases = %d, want 2", len(sink.releases))
	}
	if prog.ReleasedEvents() != 2 {
		t.Errorf("ReleasedEvents = %d, want 2", prog.ReleasedEvents())
	}

	if got := b.State(); got != CommitStateStreamOpen {
		t.Errorf("state after release = %q, want %q", got, CommitStateStreamOpen)
	}

	// Second release still works (start already consumed).
	_, err = b.ReleaseSafe(ctx, "a1", []ReleaseEvent{ev1})
	if err != nil {
		t.Fatalf("second ReleaseSafe: %v", err)
	}
	if len(sink.releases) != 3 {
		t.Errorf("total releases after second call = %d, want 3", len(sink.releases))
	}
	if len(sink.responseStarts) != 1 {
		t.Errorf("total responseStarts = %d, want 1", len(sink.responseStarts))
	}
}

// TestCommitBoundaryPreservesProgressAndMonotonicState verifies the state
// transition table for start/body/error and that successful event count is
// preserved across failures.
func TestCommitBoundaryPreservesProgressAndMonotonicState(t *testing.T) {
	ctx := context.Background()

	t.Run("success_steps_preserve_count", func(t *testing.T) {
		sink := &testSink{}
		b, _ := NewCommitBoundary(sink)
		b.BeginAttempt("a1")
		rs, _ := NewResponseStart("ch", 200, nil, testNow)
		b.StageResponseStart("a1", rs)

		evs := make([]ReleaseEvent, 5)
		for i := range evs {
			e, _ := NewReleaseTextDeltaEvent("ch", "chunk", testNow)
			evs[i] = e
		}

		prog, err := b.ReleaseSafe(ctx, "a1", evs)
		if err != nil {
			t.Fatalf("ReleaseSafe: %v", err)
		}
		if prog.ReleasedEvents() != 5 {
			t.Errorf("ReleasedEvents = %d, want 5", prog.ReleasedEvents())
		}
		if len(sink.releases) != 5 {
			t.Errorf("releases = %d, want 5", len(sink.releases))
		}
		if got := b.State(); got != CommitStateStreamOpen {
			t.Errorf("state = %q, want stream_open", got)
		}
	})

	t.Run("sink_error_preserves_progress", func(t *testing.T) {
		sink := &testSink{failAfterSuccesses: 1}
		b, _ := NewCommitBoundary(sink)
		b.BeginAttempt("a1")
		rs, _ := NewResponseStart("ch", 200, nil, testNow)
		b.StageResponseStart("a1", rs)

		evs := make([]ReleaseEvent, 3)
		for i := range evs {
			e, _ := NewReleaseTextDeltaEvent("ch", "chunk", testNow)
			evs[i] = e
		}

		prog, err := b.ReleaseSafe(ctx, "a1", evs)
		if err == nil {
			t.Error("expected error from ReleaseSafe")
		}
		// First event succeeded before the forced failure.
		if prog.ReleasedEvents() != 1 {
			t.Errorf("ReleasedEvents = %d, want 1", prog.ReleasedEvents())
		}
		if len(sink.releases) != 1 {
			t.Errorf("releases = %d, want 1", len(sink.releases))
		}
		if got := b.State(); got != CommitStateStreamOpen {
			t.Errorf("state = %q, want stream_open (preserved)", got)
		}
	})

	t.Run("begin_after_stream_open_rejected", func(t *testing.T) {
		sink := &testSink{}
		b, _ := NewCommitBoundary(sink)
		b.BeginAttempt("a1")
		rs, _ := NewResponseStart("ch", 200, nil, testNow)
		b.StageResponseStart("a1", rs)

		ev, _ := NewReleaseTextDeltaEvent("ch", "x", testNow)
		b.ReleaseSafe(ctx, "a1", []ReleaseEvent{ev})

		err := b.BeginAttempt("a2")
		if err != ErrBoundaryUncommitted {
			t.Errorf("BeginAttempt error = %v, want ErrBoundaryUncommitted", err)
		}
	})

	t.Run("zero_release_events_rejected", func(t *testing.T) {
		sink := &testSink{}
		b, _ := NewCommitBoundary(sink)
		b.BeginAttempt("a1")

		_, err := b.ReleaseSafe(ctx, "a1", nil)
		if err == nil {
			t.Error("expected error for empty events")
		}
	})
}

// TestCommitBoundaryCommitsEmptySuccessAndTerminalOnce verifies that success
// terminal without body commits start first, terminal exactly once, and
// State() does not deadlock during callbacks.
func TestCommitBoundaryCommitsEmptySuccessAndTerminalOnce(t *testing.T) {
	ctx := context.Background()

	t.Run("success_terminal_no_body", func(t *testing.T) {
		sink := &testSink{}
		b, _ := NewCommitBoundary(sink)
		b.BeginAttempt("a1")
		rs, _ := NewResponseStart("ch", 200, nil, testNow)
		b.StageResponseStart("a1", rs)

		tr, _ := NewSuccessTerminalResult("ch", testNow)
		if err := b.CommitTerminal(ctx, "a1", tr); err != nil {
			t.Fatalf("CommitTerminal: %v", err)
		}

		if len(sink.responseStarts) != 1 {
			t.Errorf("responseStarts = %d, want 1", len(sink.responseStarts))
		}
		if len(sink.terminals) != 1 {
			t.Errorf("terminals = %d, want 1", len(sink.terminals))
		}
		if got := b.State(); got != CommitStateTerminalCommitted {
			t.Errorf("state = %q, want terminal_committed", got)
		}
	})

	t.Run("second_terminal_rejected", func(t *testing.T) {
		sink := &testSink{}
		b, _ := NewCommitBoundary(sink)
		b.BeginAttempt("a1")
		tr, _ := NewSuccessTerminalResult("ch", testNow)
		b.CommitTerminal(ctx, "a1", tr)

		err := b.CommitTerminal(ctx, "a1", tr)
		if err != ErrBoundaryAlreadyTerminal {
			t.Errorf("second CommitTerminal error = %v, want ErrBoundaryAlreadyTerminal", err)
		}
	})

	t.Run("state_no_deadlock_during_callback", func(t *testing.T) {
		sink := &testSink{}
		b, _ := NewCommitBoundary(sink)
		b.BeginAttempt("a1")
		rs, _ := NewResponseStart("ch", 200, nil, testNow)
		b.StageResponseStart("a1", rs)

		done := make(chan struct{})
		go func() {
			defer close(done)
			for i := 0; i < 100; i++ {
				_ = b.State()
			}
		}()

		evs := make([]ReleaseEvent, 50)
		for i := range evs {
			e, _ := NewReleaseTextDeltaEvent("ch", "x", testNow)
			evs[i] = e
		}
		_, err := b.ReleaseSafe(ctx, "a1", evs)
		if err != nil {
			t.Fatalf("ReleaseSafe: %v", err)
		}

		<-done
	})

	t.Run("error_terminal_discards_staged_start", func(t *testing.T) {
		sink := &testSink{}
		b, _ := NewCommitBoundary(sink)
		b.BeginAttempt("a1")
		rs, _ := NewResponseStart("ch", 200, nil, testNow)
		b.StageResponseStart("a1", rs)

		desc, _ := NewExternalDescriptor("generic", "internal_error", "error", "")
		causes, _ := NewFailureCauseChain([]FailureCause{})
		tr, _ := NewErrorTerminalResult("ch", desc, causes, testNow)

		if err := b.CommitTerminal(ctx, "a1", tr); err != nil {
			t.Fatalf("CommitTerminal: %v", err)
		}

		if len(sink.responseStarts) != 0 {
			t.Errorf("responseStarts = %d, want 0 (error discards start)", len(sink.responseStarts))
		}
		if len(sink.terminals) != 1 {
			t.Errorf("terminals = %d, want 1", len(sink.terminals))
		}
	})
}

// ---------------------------------------------------------------------------
// REVIEW_API-3: Attempt replacement and recovery strategy matrix
// ---------------------------------------------------------------------------

// TestCommitBoundaryReplacesOnlyUncommittedAttempt verifies that replacement
// discards the old attempt and staged start, allowing a new attempt to begin.
func TestCommitBoundaryReplacesOnlyUncommittedAttempt(t *testing.T) {
	ctx := context.Background()

	t.Run("replacement_discards_old_and_allows_new", func(t *testing.T) {
		sink := &testSink{}
		b, _ := NewCommitBoundary(sink)

		// First attempt with staged start.
		b.BeginAttempt("old")
		rs, _ := NewResponseStart("ch", 200, nil, testNow)
		b.StageResponseStart("old", rs)

		// Replace.
		if err := b.ReplaceUncommittedAttempt("old", "new"); err != nil {
			t.Fatalf("ReplaceUncommittedAttempt: %v", err)
		}

		// Old attempt should not work (no current attempt).
		err := b.StageResponseStart("old", rs)
		if err == nil {
			// Actually after replace, there's no current attempt, so
			// StageResponseStart should work since state is still uncommitted.
			// But the old attempt ID is not special anymore.
		}

		// New attempt should work.
		b.BeginAttempt("new")
		rs2, _ := NewResponseStart("ch", 200, nil, testNow)
		b.StageResponseStart("new", rs2)

		ev, _ := NewReleaseTextDeltaEvent("ch", "data", testNow)
		_, err = b.ReleaseSafe(ctx, "new", []ReleaseEvent{ev})
		if err != nil {
			t.Fatalf("ReleaseSafe: %v", err)
		}

		// Exactly one start from new attempt.
		if len(sink.responseStarts) != 1 {
			t.Errorf("responseStarts = %d, want 1 (only new attempt)", len(sink.responseStarts))
		}
	})

	t.Run("post_open_replacement_rejected", func(t *testing.T) {
		sink := &testSink{}
		b, _ := NewCommitBoundary(sink)
		b.BeginAttempt("a1")
		rs, _ := NewResponseStart("ch", 200, nil, testNow)
		b.StageResponseStart("a1", rs)

		ev, _ := NewReleaseTextDeltaEvent("ch", "x", testNow)
		b.ReleaseSafe(ctx, "a1", []ReleaseEvent{ev})

		// After stream_open, we cannot begin a new attempt.
		err := b.BeginAttempt("a2")
		if err != ErrBoundaryUncommitted {
			t.Errorf("BeginAttempt after open error = %v, want ErrBoundaryUncommitted", err)
		}
	})
}

// TestCommitBoundaryCommitStrategyMatrix verifies the allowed strategy table
// for each commit state.
func TestCommitBoundaryCommitStrategyMatrix(t *testing.T) {
	ctx := context.Background()
	strategies := []RecoveryStrategy{
		RecoveryStrategyExactReplay,
		RecoveryStrategyContinuationRepair,
		RecoveryStrategySchemaRepair,
	}

	t.Run("transport_uncommitted", func(t *testing.T) {
		sink := &testSink{}
		b, _ := NewCommitBoundary(sink)
		b.BeginAttempt("a1")

		expectations := map[RecoveryStrategy]bool{
			RecoveryStrategyExactReplay:        true,
			RecoveryStrategySchemaRepair:       true,
			RecoveryStrategyContinuationRepair: false,
		}
		for _, s := range strategies {
			got := b.CommitAllowsStrategy(s)
			want := expectations[s]
			if got != want {
				t.Errorf("CommitAllowsStrategy(%s) = %v, want %v", s, got, want)
			}
		}
	})

	t.Run("stream_open", func(t *testing.T) {
		sink := &testSink{}
		b, _ := NewCommitBoundary(sink)
		b.BeginAttempt("a1")
		rs, _ := NewResponseStart("ch", 200, nil, testNow)
		b.StageResponseStart("a1", rs)
		ev, _ := NewReleaseTextDeltaEvent("ch", "x", testNow)
		b.ReleaseSafe(ctx, "a1", []ReleaseEvent{ev})

		expectations := map[RecoveryStrategy]bool{
			RecoveryStrategyExactReplay:        false,
			RecoveryStrategySchemaRepair:       false,
			RecoveryStrategyContinuationRepair: true,
		}
		for _, s := range strategies {
			got := b.CommitAllowsStrategy(s)
			want := expectations[s]
			if got != want {
				t.Errorf("CommitAllowsStrategy(%s) = %v, want %v", s, got, want)
			}
		}
	})

	t.Run("terminal_committed", func(t *testing.T) {
		sink := &testSink{}
		b, _ := NewCommitBoundary(sink)
		b.BeginAttempt("a1")
		tr, _ := NewSuccessTerminalResult("ch", testNow)
		b.CommitTerminal(ctx, "a1", tr)

		for _, s := range strategies {
			got := b.CommitAllowsStrategy(s)
			if got {
				t.Errorf("CommitAllowsStrategy(%s) = true, want false after terminal", s)
			}
		}
	})
}
