package streamgate

import (
	"context"
	"errors"
	"fmt"
	"sync"
	"sync/atomic"
	"testing"
	"time"
)

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

// testCanceller records cancel attempts and can be programmed to fail.
type testCanceller struct {
	cancelCount int
	cancelErr   error
}

func (c *testCanceller) CancelAttempt(ctx context.Context, attemptID string) error {
	c.cancelCount++
	return c.cancelErr
}

// setupReleaser creates a StreamReleaser with a rolling_window EvidenceTail,
// CommitBoundary, and canceller. The tail is configured so that appending
// enough events triggers an epoch via the rolling threshold.
//
// evidenceRunes controls the rolling threshold; maxBuffer is the hard limit.
func setupReleaser(t *testing.T, channel string, evidenceRunes, maxBuffer int) (*StreamReleaser, *testSink, *testCanceller) {
	t.Helper()

	// Create a rolling_window plan with a low threshold so a few events
	// trigger an epoch quickly.
	kinds := []EventKind{EventKindTextDelta, EventKindReasoningDelta}
	var req FilterHoldRequirement
	if maxBuffer > 0 {
		var err error
		req, err = NewFilterHoldRequirementRollingWithMaxBuffer(channel, kinds, evidenceRunes, maxBuffer)
		if err != nil {
			t.Fatalf("NewFilterHoldRequirementRollingWithMaxBuffer: %v", err)
		}
	} else {
		var err error
		req, err = NewFilterHoldRequirementRolling(channel, kinds, evidenceRunes)
		if err != nil {
			t.Fatalf("NewFilterHoldRequirementRolling: %v", err)
		}
	}
	binding, err := NewFilterHoldBinding("f1", req, true)
	if err != nil {
		t.Fatalf("NewFilterHoldBinding: %v", err)
	}
	plan, err := NewEvidencePlan([]FilterHoldBinding{binding})
	if err != nil {
		t.Fatalf("NewEvidencePlan: %v", err)
	}

	tail, err := NewEvidenceTail(plan)
	if err != nil {
		t.Fatalf("NewEvidenceTail: %v", err)
	}

	sink := &testSink{}
	boundary, err := NewCommitBoundary(sink)
	if err != nil {
		t.Fatalf("NewCommitBoundary: %v", err)
	}
	canceller := &testCanceller{}

	releaser, err := NewStreamReleaser(tail, boundary, canceller)
	if err != nil {
		t.Fatalf("NewStreamReleaser: %v", err)
	}

	return releaser, sink, canceller
}

// appendDataEvents creates and appends text_delta events to the tail until
// an epoch is triggered (rolling threshold reached). Returns the epoch.
func appendDataEvents(t *testing.T, tail *EvidenceTail, channel string, count int) EvidenceEpoch {
	t.Helper()
	var epoch EvidenceEpoch
	for i := 0; i < count; i++ {
		ne, err := NewTextDeltaEvent(channel, "chunk", releaserNow)
		if err != nil {
			t.Fatalf("NewTextDeltaEvent: %v", err)
		}
		e, signal, err := tail.Append(ne)
		if err != nil {
			t.Fatalf("Append: %v", err)
		}
		if signal == EvidenceTailSignalThreshold {
			epoch = e
		}
	}
	return epoch
}

// createTextReleaseEvent creates a ReleaseEvent of kind text_delta.
func createTextReleaseEvent(channel, text string) ReleaseEvent {
	ev, err := NewReleaseTextDeltaEvent(channel, text, releaserNow)
	if err != nil {
		panic(err)
	}
	return ev
}

// createSuccessTerminalResult creates a TerminalResult of kind success.
func createSuccessTerminalResult(channel string) TerminalResult {
	tr, err := NewSuccessTerminalResult(channel, releaserNow)
	if err != nil {
		panic(err)
	}
	return tr
}

// createErrorTerminalResult creates an error TerminalResult.
func createErrorTerminalResult(channel string) TerminalResult {
	desc, err := NewExternalDescriptor("generic", "internal_error", "an_internal_error", "")
	if err != nil {
		panic(fmt.Sprintf("createErrorTerminalResult NewExternalDescriptor failed: %v", err))
	}
	causes, err := NewFailureCauseChain([]FailureCause{})
	if err != nil {
		panic(fmt.Sprintf("createErrorTerminalResult NewFailureCauseChain failed: %v", err))
	}
	tr, err := NewErrorTerminalResult(channel, desc, causes, releaserNow)
	if err != nil {
		panic(fmt.Sprintf("createErrorTerminalResult NewErrorTerminalResult failed: %v", err))
	}
	return tr
}

// ---------------------------------------------------------------------------
// REVIEW_API-2: Evidence prepare/confirm with sink progress
// ---------------------------------------------------------------------------

// TestStreamReleaserConfirmsExactlySinkProgress verifies zero/partial/full
// sink progress where confirmed cursor matches sink success count.
func TestStreamReleaserConfirmsExactlySinkProgress(t *testing.T) {
	ctx := context.Background()

	t.Run("zero_sink_progress", func(t *testing.T) {
		releaser, sink, _ := setupReleaser(t, "ch-zero", 2, 100)

		// Begin attempt and stage response start on boundary.
		releaser.boundary.BeginAttempt("a1")
		rs, _ := NewResponseStart("ch-zero", 200, nil, releaserNow)
		releaser.boundary.StageResponseStart("a1", rs)

		// Append data events to trigger epoch.
		epoch := appendDataEvents(t, releaser.tail, "ch-zero", 3)

		// All releases fail.
		sink.failAll = true

		prog, err := releaser.ReleaseEpoch(ctx, "a1", epoch.ID())
		if err == nil {
			t.Error("expected error from ReleaseEpoch")
		}
		if prog.ReleasedEvents() != 0 {
			t.Errorf("ReleasedEvents = %d, want 0 (all events failed)", prog.ReleasedEvents())
		}
	})

	t.Run("partial_sink_progress", func(t *testing.T) {
		releaser, sink, _ := setupReleaser(t, "ch-partial", 2, 100)

		releaser.boundary.BeginAttempt("a1")
		rs, _ := NewResponseStart("ch-partial", 200, nil, releaserNow)
		releaser.boundary.StageResponseStart("a1", rs)

		epoch := appendDataEvents(t, releaser.tail, "ch-partial", 5)

		// 2 successes, then fail.
		sink.failAfterSuccesses = 2

		prog, err := releaser.ReleaseEpoch(ctx, "a1", epoch.ID())
		if err == nil {
			t.Error("expected partial error")
		}
		if prog.ReleasedEvents() != 2 {
			t.Errorf("ReleasedEvents = %d, want 2", prog.ReleasedEvents())
		}
	})

	t.Run("partial_sink_progress_two_succeed_then_fail", func(t *testing.T) {
		releaser, sink, _ := setupReleaser(t, "ch-partial2", 2, 100)

		releaser.boundary.BeginAttempt("a1")
		rs, _ := NewResponseStart("ch-partial2", 200, nil, releaserNow)
		releaser.boundary.StageResponseStart("a1", rs)

		epoch := appendDataEvents(t, releaser.tail, "ch-partial2", 5)

		// 2 successes, then fail.
		sink.failAfterSuccesses = 2

		prog, err := releaser.ReleaseEpoch(ctx, "a1", epoch.ID())
		if err == nil {
			t.Error("expected partial error")
		}
		if prog.ReleasedEvents() != 2 {
			t.Errorf("ReleasedEvents = %d, want 2", prog.ReleasedEvents())
		}
	})

	t.Run("full_sink_progress", func(t *testing.T) {
		releaser, sink, _ := setupReleaser(t, "ch-full", 2, 100)

		releaser.boundary.BeginAttempt("a1")
		rs, _ := NewResponseStart("ch-full", 200, nil, releaserNow)
		releaser.boundary.StageResponseStart("a1", rs)

		epoch := appendDataEvents(t, releaser.tail, "ch-full", 3)

		prog, err := releaser.ReleaseEpoch(ctx, "a1", epoch.ID())
		if err != nil {
			t.Fatalf("ReleaseEpoch: %v", err)
		}
		if prog.ReleasedEvents() != 3 {
			t.Errorf("ReleasedEvents = %d, want 3", prog.ReleasedEvents())
		}
		// Cursor should advance by 3.
		if got := releaser.tail.CommittedCursor("ch-full"); got != 3 {
			t.Errorf("committed cursor = %d, want 3", got)
		}
		// Sink should have received 3 releases + 1 start.
		if len(sink.releases) != 3 {
			t.Errorf("sink releases = %d, want 3", len(sink.releases))
		}
		if len(sink.responseStarts) != 1 {
			t.Errorf("sink responseStarts = %d, want 1", len(sink.responseStarts))
		}
	})
}

// TestStreamReleaserTerminalBeforeEvidenceExposesOneOutcome verifies that the
// pass path exposes start/output+success terminal and error path exposes
// only error terminal without releasing pending/start.
func TestStreamReleaserTerminalBeforeEvidenceExposesOneOutcome(t *testing.T) {
	ctx := context.Background()

	t.Run("pass_exposes_start_and_terminal", func(t *testing.T) {
		releaser, sink, _ := setupReleaser(t, "ch-pass", 2, 100)

		releaser.boundary.BeginAttempt("a1")
		rs, _ := NewResponseStart("ch-pass", 200, nil, releaserNow)
		releaser.boundary.StageResponseStart("a1", rs)

		// Create and release an epoch.
		epoch := appendDataEvents(t, releaser.tail, "ch-pass", 2)
		prog, err := releaser.ReleaseEpoch(ctx, "a1", epoch.ID())
		if err != nil {
			t.Fatalf("ReleaseEpoch: %v", err)
		}
		if prog.ReleasedEvents() != 2 {
			t.Errorf("ReleasedEvents = %d, want 2", prog.ReleasedEvents())
		}

		// Terminal success.
		tr := createSuccessTerminalResult("ch-pass")
		prog2, err := releaser.ReleaseTerminalEpoch(ctx, "a1", epoch.ID(), tr)
		if err != nil {
			t.Fatalf("ReleaseTerminalEpoch: %v", err)
		}
		_ = prog2

		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 len(sink.releases) != 2 {
			t.Errorf("releases = %d, want 2", len(sink.releases))
		}
	})

	t.Run("error_exposes_only_error_terminal", func(t *testing.T) {
		releaser, sink, canceller := setupReleaser(t, "ch-err", 2, 100)

		releaser.boundary.BeginAttempt("a2")
		rs, _ := NewResponseStart("ch-err", 200, nil, releaserNow)
		releaser.boundary.StageResponseStart("a2", rs)

		// Create some pending events but don't release them.
		appendDataEvents(t, releaser.tail, "ch-err", 2)

		// Error terminal without releasing events.
		tr := createErrorTerminalResult("ch-err")
		prog, err := releaser.ReleaseTerminalEpoch(ctx, "a2", 999, tr)
		if err != nil {
			t.Fatalf("ReleaseTerminalEpoch: %v", err)
		}
		_ = prog

		// Should have called cancel.
		if canceller.cancelCount != 1 {
			t.Errorf("cancelCount = %d, want 1", canceller.cancelCount)
		}

		if len(sink.responseStarts) != 0 {
			t.Errorf("responseStarts = %d, want 0 (error path)", len(sink.responseStarts))
		}
		if len(sink.terminals) != 1 {
			t.Errorf("terminals = %d, want 1", len(sink.terminals))
		}
		if len(sink.releases) != 0 {
			t.Errorf("releases = %d, want 0 (no pending release)", len(sink.releases))
		}
	})
}

// TestStreamReleaserDoesNotDuplicatePreparedRelease verifies that repeated
// epoch calls do not duplicate downstream events.
func TestStreamReleaserDoesNotDuplicatePreparedRelease(t *testing.T) {
	ctx := context.Background()
	releaser, sink, _ := setupReleaser(t, "ch-dup", 2, 100)

	releaser.boundary.BeginAttempt("a1")
	rs, _ := NewResponseStart("ch-dup", 200, nil, releaserNow)
	releaser.boundary.StageResponseStart("a1", rs)

	// First epoch.
	epoch1 := appendDataEvents(t, releaser.tail, "ch-dup", 2)
	prog, err := releaser.ReleaseEpoch(ctx, "a1", epoch1.ID())
	if err != nil {
		t.Fatalf("ReleaseEpoch 1: %v", err)
	}
	if prog.ReleasedEvents() != 2 {
		t.Errorf("first ReleasedEvents = %d, want 2", prog.ReleasedEvents())
	}

	// Second epoch (more events to trigger another).
	epoch2 := appendDataEvents(t, releaser.tail, "ch-dup", 2)
	prog2, err := releaser.ReleaseEpoch(ctx, "a1", epoch2.ID())
	if err != nil {
		t.Fatalf("ReleaseEpoch 2: %v", err)
	}
	if prog2.ReleasedEvents() != 2 {
		t.Errorf("second ReleasedEvents = %d, want 2", prog2.ReleasedEvents())
	}

	// Total should be 4 releases (2+2), no duplicates.
	if len(sink.releases) != 4 {
		t.Errorf("total releases = %d, want 4 (2+2)", len(sink.releases))
	}
}

// ---------------------------------------------------------------------------
// REVIEW_API-3: Attempt replacement, idle and overflow
// ---------------------------------------------------------------------------

// TestStreamReleaserReplacementResetsTailGeneration verifies that boundary
// attempt and tail pending/prepared token are replaced together.
func TestStreamReleaserReplacementResetsTailGeneration(t *testing.T) {
	ctx := context.Background()
	releaser, _, _ := setupReleaser(t, "ch-rep", 2, 100)

	// Begin attempt and stage start.
	releaser.boundary.BeginAttempt("old")
	rs, _ := NewResponseStart("ch-rep", 200, nil, releaserNow)
	releaser.boundary.StageResponseStart("old", rs)

	// Append events.
	appendDataEvents(t, releaser.tail, "ch-rep", 2)

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

	// Tail cursor should be 0 after replace.
	if got := releaser.tail.CommittedCursor("ch-rep"); got != 0 {
		t.Errorf("tail cursor after replace = %d, want 0", got)
	}
	// Boundary attempt should be cleared.
	if got := releaser.boundary.CurrentAttempt(); got != "" {
		t.Errorf("boundary attempt after replace = %q, want empty", got)
	}
}

// TestStreamReleaserIdleReleasesNoPending verifies that idle error results
// in zero release/start and correct cancel→terminal ordering.
func TestStreamReleaserIdleReleasesNoPending(t *testing.T) {
	ctx := context.Background()
	releaser, sink, canceller := setupReleaser(t, "ch-idle", 2, 100)

	releaser.boundary.BeginAttempt("a1")
	rs, _ := NewResponseStart("ch-idle", 200, nil, releaserNow)
	releaser.boundary.StageResponseStart("a1", rs)

	// Append some events to create pending state.
	appendDataEvents(t, releaser.tail, "ch-idle", 2)

	// FailPending.
	tr := createErrorTerminalResult("ch-idle")
	prog, err := releaser.FailPending(ctx, "a1", tr)
	if err != nil {
		t.Fatalf("FailPending: %v", err)
	}
	if prog.ReleasedEvents() != 0 {
		t.Errorf("ReleasedEvents = %d, want 0", prog.ReleasedEvents())
	}

	// No responses or releases.
	if len(sink.responseStarts) != 0 {
		t.Errorf("responseStarts = %d, want 0", len(sink.responseStarts))
	}
	if len(sink.releases) != 0 {
		t.Errorf("releases = %d, want 0", len(sink.releases))
	}
	// Cancel should have been called.
	if canceller.cancelCount != 1 {
		t.Errorf("cancelCount = %d, want 1", canceller.cancelCount)
	}
	if len(sink.terminals) != 1 {
		t.Errorf("terminals = %d, want 1", len(sink.terminals))
	}
}

// TestStreamReleaserOverflowCancelsBeforeSingleTerminal verifies that S19
// overflow results in no partial release, and terminal exactly once.
func TestStreamReleaserOverflowCancelsBeforeSingleTerminal(t *testing.T) {
	ctx := context.Background()
	// Use very small buffer to trigger overflow.
	// rolling_window with evidence 1 and maxBuffer 10 (minimum).
	releaser, sink, canceller := setupReleaser(t, "ch-overflow", 1, 10)

	// Append events, expecting overflow signal.
	overflowed := false
	for i := 0; i < 50; i++ {
		ne, err := NewTextDeltaEvent("ch-overflow", "chunk", releaserNow)
		if err != nil {
			t.Fatalf("NewTextDeltaEvent: %v", err)
		}
		_, signal, err := releaser.tail.Append(ne)
		if err != nil {
			t.Fatalf("Append: %v", err)
		}
		if signal == EvidenceTailSignalBufferOverflow {
			overflowed = true
			break
		}
	}
	if !overflowed {
		t.Skip("expected overflow signal not generated with current configuration")
	}

	// FailPending for overflow recovery.
	tr := createErrorTerminalResult("ch-overflow")
	prog, err := releaser.FailPending(ctx, "a1", tr)
	if err != nil {
		t.Fatalf("FailPending: %v", err)
	}
	if prog.ReleasedEvents() != 0 {
		t.Errorf("ReleasedEvents = %d, want 0 (overflow)", prog.ReleasedEvents())
	}

	if len(sink.terminals) != 1 {
		t.Errorf("terminals = %d, want 1 (exactly once)", len(sink.terminals))
	}
	_ = canceller
}

// TestStreamReleaserCancelFailureAppendsBoundedCause verifies that when
// cancel fails, the cause is appended to the terminal result.
func TestStreamReleaserCancelFailureAppendsBoundedCause(t *testing.T) {
	ctx := context.Background()
	releaser, sink, canceller := setupReleaser(t, "ch-cancel-fail", 2, 100)

	releaser.boundary.BeginAttempt("a1")
	rs, _ := NewResponseStart("ch-cancel-fail", 200, nil, releaserNow)
	releaser.boundary.StageResponseStart("a1", rs)

	// Program canceller to fail.
	canceller.cancelErr = errors.New("test: cancel failed")

	appendDataEvents(t, releaser.tail, "ch-cancel-fail", 2)

	tr := createErrorTerminalResult("ch-cancel-fail")
	prog, err := releaser.FailPending(ctx, "a1", tr)
	if err != nil {
		t.Fatalf("FailPending: %v", err)
	}
	_ = prog

	// Terminal should still be committed exactly once.
	if len(sink.terminals) != 1 {
		t.Errorf("terminals = %d, want 1", len(sink.terminals))
	}
	// The terminal result should include the cancel failure cause.
	lastTerminal := sink.terminals[len(sink.terminals)-1]
	causes := lastTerminal.FailureCauses()
	if causes.Len() == 0 {
		t.Error("expected failure causes to include cancel failure")
	} else {
		found := false
		for i := 0; i < causes.Len(); i++ {
			c, _ := causes.At(i)
			if c.Code() == "attempt_cancel_failed" {
				found = true
				break
			}
		}
		if !found {
			t.Error("expected 'attempt_cancel_failed' cause code")
		}
	}
}

// TestStreamReleaserConcurrentEpochsAreSerialized verifies that concurrent
// release calls through the releaser serializer do not cause data races
// (the tail itself is not thread-safe, so appends must be serialized).
func TestStreamReleaserConcurrentEpochsAreSerialized(t *testing.T) {
	ctx := context.Background()
	releaser, _, _ := setupReleaser(t, "ch-conc", 2, 100)

	releaser.boundary.BeginAttempt("a1")
	rs, _ := NewResponseStart("ch-conc", 200, nil, releaserNow)
	releaser.boundary.StageResponseStart("a1", rs)

	// Pre-create all epochs sequentially (tail is not thread-safe).
	epochs := make([]EvidenceEpoch, 5)
	for i := 0; i < 5; i++ {
		epochs[i] = appendDataEvents(t, releaser.tail, "ch-conc", 2)
	}

	var wg sync.WaitGroup
	var successful int64
	var errors int64

	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func(idx int) {
			defer wg.Done()
			_, err := releaser.ReleaseEpoch(ctx, "a1", epochs[idx].ID())
			if err == nil {
				atomic.AddInt64(&successful, 1)
			} else {
				atomic.AddInt64(&errors, 1)
			}
		}(i)
	}
	wg.Wait()

	// All events should have been processed without race.
	total := atomic.LoadInt64(&successful) + atomic.LoadInt64(&errors)
	if total != 5 {
		t.Errorf("total results = %d, want 5", total)
	}
}

// TestStreamReleaserTerminalAfterFullReleaseSkipsSecondaryRelease verifies
// that after an epoch is fully released and confirmed, ReleaseTerminalEpoch
// does not attempt another release.
func TestStreamReleaserTerminalAfterFullReleaseSkipsSecondaryRelease(t *testing.T) {
	ctx := context.Background()
	releaser, sink, _ := setupReleaser(t, "ch-skip", 2, 100)

	releaser.boundary.BeginAttempt("a1")
	rs, _ := NewResponseStart("ch-skip", 200, nil, releaserNow)
	releaser.boundary.StageResponseStart("a1", rs)

	// Release epoch fully.
	epoch := appendDataEvents(t, releaser.tail, "ch-skip", 2)
	prog, err := releaser.ReleaseEpoch(ctx, "a1", epoch.ID())
	if err != nil {
		t.Fatalf("ReleaseEpoch: %v", err)
	}
	if prog.ReleasedEvents() != 2 {
		t.Errorf("ReleasedEvents = %d, want 2", prog.ReleasedEvents())
	}

	// Terminal after full release.
	tr := createSuccessTerminalResult("ch-skip")
	prog2, err := releaser.ReleaseTerminalEpoch(ctx, "a1", epoch.ID(), tr)
	if err != nil {
		t.Fatalf("ReleaseTerminalEpoch: %v", err)
	}
	_ = prog2

	// Should have 2 releases + 1 start, no extra.
	if len(sink.releases) != 2 {
		t.Errorf("releases = %d, want 2 (no secondary release)", len(sink.releases))
	}
	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))
	}
}
