package control

import (
	"context"
	"errors"
	"strings"
	"sync/atomic"
	"testing"
	"time"

	"reasonix/internal/event"
)

// holdFinishingWindow returns a sink that blocks inside the FIRST TurnDone
// delivery until release is closed, holding the controller's finishing window
// deterministically open so tests can place submits inside it. Later
// TurnDones pass through unblocked.
func holdFinishingWindow(release <-chan struct{}, entered chan<- struct{}, events chan<- event.Event) event.Sink {
	var first atomic.Int32
	return event.FuncSink(func(e event.Event) {
		if e.Kind == event.TurnDone && first.Add(1) == 1 {
			entered <- struct{}{}
			<-release
		}
		if events != nil {
			select {
			case events <- e:
			default:
			}
		}
	})
}

// TestParkedTurnsRunFIFO pins ordering: several submits landing inside one
// finishing window run in arrival order, one per window close, none lost.
func TestParkedTurnsRunFIFO(t *testing.T) {
	entered := make(chan struct{}, 1)
	release := make(chan struct{})
	c := New(Options{Sink: holdFinishingWindow(release, entered, nil)})

	c.runGuarded(func(context.Context) error { return nil })
	<-entered

	var order []int
	ran := make(chan int, 3)
	for i := 1; i <= 3; i++ {

		if got := c.runGuarded(func(context.Context) error {
			ran <- i
			return nil
		}); got != turnParked {
			t.Fatalf("submit %d admission = %v, want turnParked", i, got)
		}
	}
	close(release)

	deadline := time.After(30 * time.Second)
	for len(order) < 3 {
		select {
		case i := <-ran:
			order = append(order, i)
		case <-deadline:
			t.Fatalf("parked turns did not all run; got order %v", order)
		}
	}
	if order[0] != 1 || order[1] != 2 || order[2] != 3 {
		t.Fatalf("parked turns ran out of order: %v", order)
	}
}

// TestSubmitDuringRotationEmitsNotice pins the rotating posture: the input's
// intended session is ambiguous while the executor session is being swapped,
// so the submit is refused with a user-visible notice instead of silently
// dropped (the caller should resend against the session they can now see).
func TestSubmitDuringRotationEmitsNotice(t *testing.T) {
	events := make(chan event.Event, 8)
	c := New(Options{Sink: event.FuncSink(func(e event.Event) {
		select {
		case events <- e:
		default:
		}
	})})

	c.mu.Lock()
	c.rotating = true
	c.mu.Unlock()

	bodyRan := make(chan struct{}, 1)
	if got := c.runGuarded(func(context.Context) error {
		bodyRan <- struct{}{}
		return nil
	}); got != turnDroppedRotating {
		t.Fatalf("admission during rotation = %v, want turnDroppedRotating", got)
	}

	select {
	case e := <-events:
		if e.Kind != event.Notice || e.Level != event.LevelWarn || !strings.Contains(e.Text, "resend") {
			t.Fatalf("event = %+v, want a warn notice asking to resend", e)
		}
	case <-time.After(time.Second):
		t.Fatal("no notice emitted for a rotation-dropped submit")
	}
	select {
	case <-bodyRan:
		t.Fatal("rotation-dropped body must not run")
	case <-time.After(50 * time.Millisecond):
	}

	c.mu.Lock()
	c.rotating = false
	c.mu.Unlock()
}

// TestSubmitWhileRunningStaysSilentNoOp pins the running posture: unchanged
// from the historical contract — frontends own the steer/queue UX, internal
// opportunistic callers rely on the quiet no-op.
func TestSubmitWhileRunningStaysSilentNoOp(t *testing.T) {
	block := make(chan struct{})
	var notices atomic.Int32
	c := New(Options{Sink: event.FuncSink(func(e event.Event) {
		if e.Kind == event.Notice {
			notices.Add(1)
		}
	})})

	started := make(chan struct{})
	c.runGuarded(func(context.Context) error {
		close(started)
		<-block
		return nil
	})
	<-started

	if got := c.runGuarded(func(context.Context) error { return nil }); got != turnDroppedRunning {
		t.Fatalf("admission while running = %v, want turnDroppedRunning", got)
	}
	if n := notices.Load(); n != 0 {
		t.Fatalf("running drop should stay silent, got %d notices", n)
	}
	close(block)
	waitIdleAdmission(t, c)
}

// TestCloseDiscardsParkedTurns pins teardown: a turn parked in the finishing
// window must not start against a controller that has been closed.
func TestCloseDiscardsParkedTurns(t *testing.T) {
	entered := make(chan struct{}, 1)
	release := make(chan struct{})
	c := New(Options{Sink: holdFinishingWindow(release, entered, nil)})

	c.runGuarded(func(context.Context) error { return nil })
	<-entered

	parkedRan := make(chan struct{}, 1)
	if got := c.runGuarded(func(context.Context) error {
		parkedRan <- struct{}{}
		return nil
	}); got != turnParked {
		t.Fatalf("admission = %v, want turnParked", got)
	}

	c.Close()
	close(release)

	select {
	case <-parkedRan:
		t.Fatal("parked turn ran after Close")
	case <-time.After(200 * time.Millisecond):
	}
}

// TestCloseSealsAdmissionDuringFinishingWindow pins the terminal-state
// ordering the first review round flagged: Close clears the parked queue, but
// a submit arriving AFTER that — while the old turn's TurnDone delivery is
// still in flight — must be rejected outright, not parked and started against
// freed resources when the window closes.
func TestCloseSealsAdmissionDuringFinishingWindow(t *testing.T) {
	entered := make(chan struct{}, 1)
	release := make(chan struct{})
	c := New(Options{Sink: holdFinishingWindow(release, entered, nil)})

	c.runGuarded(func(context.Context) error { return nil })
	<-entered // finishing window is now held open

	c.Close() // seals admission; parked queue is empty at this instant

	lateRan := make(chan struct{}, 1)
	if got := c.runGuarded(func(context.Context) error {
		lateRan <- struct{}{}
		return nil
	}); got != turnDroppedClosed {
		t.Fatalf("submit after Close during finishing window = %v, want turnDroppedClosed", got)
	}

	close(release) // window closes; the drain must start nothing
	select {
	case <-lateRan:
		t.Fatal("submit accepted after Close ran when finishing window closed")
	case <-time.After(200 * time.Millisecond):
	}
}

// TestRunTurnRefusedDuringFinishingWindow pins the synchronous gate: RunTurn
// must not start inside the previous turn's TurnDone delivery window — that
// would recreate the completion/transport crosstalk the window prevents.
func TestRunTurnRefusedDuringFinishingWindow(t *testing.T) {
	entered := make(chan struct{}, 1)
	release := make(chan struct{})
	c := New(Options{Sink: holdFinishingWindow(release, entered, nil)})

	c.runGuarded(func(context.Context) error { return nil })
	<-entered // finishing window is now held open

	errCh := make(chan error, 1)
	go func() { errCh <- c.RunTurn(context.Background(), "sync input") }()
	select {
	case err := <-errCh:
		if !errors.Is(err, ErrTurnRunning) {
			t.Fatalf("RunTurn during finishing window = %v, want ErrTurnRunning", err)
		}
	case <-time.After(time.Second):
		t.Fatal("RunTurn did not return promptly during the finishing window")
	}
	close(release)
	waitIdleAdmission(t, c)
}

// TestRunTurnRefusedAfterClose pins the terminal state for the synchronous
// entry point too.
func TestRunTurnRefusedAfterClose(t *testing.T) {
	c := New(Options{})
	c.Close()
	if err := c.RunTurn(context.Background(), "late"); !errors.Is(err, ErrTurnRunning) {
		t.Fatalf("RunTurn after Close = %v, want ErrTurnRunning", err)
	}
}

// waitIdleAdmission polls the running||finishing admission gate; a test that
// submits or asserts idle right after TurnDone must wait the finishing window
// out (TurnDone is emitted inside it).
func waitIdleAdmission(t *testing.T, c *Controller) {
	t.Helper()
	deadline := time.Now().Add(30 * time.Second)
	for c.Running() {
		if time.Now().After(deadline) {
			t.Fatal("timed out waiting for the controller to return to idle")
		}
		time.Sleep(time.Millisecond)
	}
}
