package transport

import (
	"context"
	"path/filepath"
	"sync"
	"sync/atomic"
	"testing"
	"time"

	"github.com/flothus/tmux-xterm-research/server-go/internal/harness/envelope"
	"github.com/flothus/tmux-xterm-research/server-go/internal/harness/event"
	"github.com/flothus/tmux-xterm-research/server-go/internal/harness/store"
)

func newQ(t *testing.T) *Queue {
	t.Helper()
	tmp := t.TempDir()
	st, err := store.Open(filepath.Join(tmp, "harness.db"))
	if err != nil {
		t.Fatalf("open store: %v", err)
	}
	t.Cleanup(func() { st.Close() })
	bus := event.NewBus(st)
	return New(st, bus)
}

func mkMsg(id, to string, prio int) *envelope.Envelope {
	e := &envelope.Envelope{
		ID: id, RunID: "run-1", From: "a", To: to,
		Type: envelope.TypeDelegate, TTLMs: 60000, Priority: prio,
		Payload: envelope.Payload{Intent: "x", Expects: envelope.ExpectsReport},
	}
	return e
}

func TestSendReceiveAck(t *testing.T) {
	q := newQ(t)
	ctx := context.Background()
	if err := q.Send(ctx, mkMsg("m1", "b", 0)); err != nil {
		t.Fatal(err)
	}
	got, err := q.Receive(ctx, "b")
	if err != nil {
		t.Fatal(err)
	}
	if got == nil || got.ID != "m1" {
		t.Fatalf("got %+v", got)
	}
	if err := q.Ack(ctx, "m1"); err != nil {
		t.Fatal(err)
	}
	// No more.
	none, err := q.Receive(ctx, "b")
	if err != nil {
		t.Fatal(err)
	}
	if none != nil {
		t.Fatalf("expected nil after ack, got %+v", none)
	}
}

func TestInterruptPriorityBeatsDelegate(t *testing.T) {
	q := newQ(t)
	ctx := context.Background()
	// Enqueue a normal delegate first, then an interrupt second.
	if err := q.Send(ctx, mkMsg("m-normal", "b", 0)); err != nil {
		t.Fatal(err)
	}
	intr := &envelope.Envelope{
		ID: "m-intr", RunID: "run-1", From: "a", To: "b",
		Type: envelope.TypeInterrupt, TaskID: "t1", TTLMs: 5000,
		Payload: envelope.Payload{Expects: envelope.ExpectsAck, Reason: "stop"},
	}
	if err := q.Send(ctx, intr); err != nil {
		t.Fatal(err)
	}

	got, err := q.Receive(ctx, "b")
	if err != nil {
		t.Fatal(err)
	}
	if got == nil || got.ID != "m-intr" {
		t.Fatalf("expected interrupt first, got %+v", got)
	}
}

func TestVisibilityRequeueAfterCrash(t *testing.T) {
	q := newQ(t)
	q.VisibilityTimeout = 20 * time.Millisecond
	ctx := context.Background()
	if err := q.Send(ctx, mkMsg("m1", "b", 0)); err != nil {
		t.Fatal(err)
	}
	got, _ := q.Receive(ctx, "b")
	if got == nil {
		t.Fatal("first receive nil")
	}
	// Don't ack — simulate crash. After timeout the reaper reclaims.
	time.Sleep(40 * time.Millisecond)
	if _, err := q.ReclaimExpired(ctx); err != nil {
		t.Fatal(err)
	}
	again, err := q.Receive(ctx, "b")
	if err != nil {
		t.Fatal(err)
	}
	if again == nil || again.ID != "m1" {
		t.Fatalf("expected redelivery, got %+v", again)
	}
}

func TestNackRetriesThenFails(t *testing.T) {
	q := newQ(t)
	q.MaxAttempts = 2
	q.BackoffBase = 1 * time.Millisecond
	q.BackoffMax = 1 * time.Millisecond
	ctx := context.Background()
	if err := q.Send(ctx, mkMsg("m1", "b", 0)); err != nil {
		t.Fatal(err)
	}
	// Receive twice, nack each. On the second nack, attempts==2 >= MaxAttempts → failed.
	for i := 0; i < 2; i++ {
		got, err := q.Receive(ctx, "b")
		if err != nil {
			t.Fatal(err)
		}
		if got == nil {
			t.Fatalf("nil on receive iteration %d", i)
		}
		if err := q.Nack(ctx, "m1", "test"); err != nil {
			t.Fatal(err)
		}
		// short backoff
		time.Sleep(3 * time.Millisecond)
	}
	// Message should now be failed; no more visible.
	none, _ := q.Receive(ctx, "b")
	if none != nil {
		t.Fatalf("expected nil after fail, got %+v", none)
	}
}

// TestReclaimExpiredDeadLettersCrashLoops is the regression test for V14: a
// worker that picks up a message and silently crashes (never Nacks) used to
// hold the message in an infinite Reclaim→Receive→crash loop, because
// ReclaimExpired just requeued without checking attempts. The result was a
// message that perpetually consumed worker turns instead of dead-lettering.
//
// After the fix, once attempts >= MaxAttempts the reaper marks the message
// failed and enqueues a dead-letter notice to the sender.
func TestReclaimExpiredDeadLettersCrashLoops(t *testing.T) {
	q := newQ(t)
	q.MaxAttempts = 2
	q.VisibilityTimeout = 10 * time.Millisecond
	ctx := context.Background()
	msg := mkMsg("m1", "bob", 0)
	msg.From = "alice"
	if err := q.Send(ctx, msg); err != nil {
		t.Fatal(err)
	}
	// Simulate two pickup-then-crash cycles. Each Receive bumps attempts;
	// no Ack/Nack means the visibility lease expires and ReclaimExpired
	// puts it back.
	for i := 0; i < 2; i++ {
		got, err := q.Receive(ctx, "bob")
		if err != nil {
			t.Fatal(err)
		}
		if got == nil {
			t.Fatalf("iteration %d: nil receive", i)
		}
		// Worker "crashes" — no Nack, no Ack.
		time.Sleep(15 * time.Millisecond)
		if _, err := q.ReclaimExpired(ctx); err != nil {
			t.Fatal(err)
		}
	}
	// Now attempts==2 and the message has been reclaimed back to queued.
	// A third Receive would push attempts to 3 and a fourth to 4 etc., but
	// the system has no mechanism to stop the loop. We assert that the
	// reaper (this iteration) noticed attempts>=MaxAttempts and dead-lettered.
	none, err := q.Receive(ctx, "bob")
	if err != nil {
		t.Fatal(err)
	}
	if none != nil {
		t.Errorf("after MaxAttempts crash cycles message is still deliverable (id=%s, attempts in DB) — crash loop not closed", none.ID)
	}
	// Sender must have received the dead-letter notice.
	notice, err := q.Receive(ctx, "alice")
	if err != nil {
		t.Fatal(err)
	}
	if notice == nil {
		t.Fatalf("sender 'alice' got no dead-letter notice after crash-loop exhaustion")
	}
	if notice.Type != envelope.TypeNack {
		t.Errorf("dead-letter notice type = %s, want nack", notice.Type)
	}
}

// TestTTLEnforcement is the V54 regression: messages whose TTL has elapsed
// must NOT be delivered, and the SweepExpiredTTL reaper must mark them
// failed. Without this, the queue table accumulates undeliverable rows
// forever and live messages can be delivered long after they're useful.
func TestTTLEnforcement(t *testing.T) {
	q := newQ(t)
	ctx := context.Background()

	// Build a message manually so we can use a short TTL.
	msg := mkMsg("m-ttl", "bob", 0)
	msg.TTLMs = 50 // 50ms — expire almost immediately
	if err := q.Send(ctx, msg); err != nil {
		t.Fatal(err)
	}
	// Wait past the TTL.
	time.Sleep(80 * time.Millisecond)

	// Receive should NOT return the expired message.
	got, err := q.Receive(ctx, "bob")
	if err != nil {
		t.Fatal(err)
	}
	if got != nil {
		t.Errorf("Receive returned a TTL-expired message: %+v", got)
	}

	// Sweep should mark it failed.
	swept, err := q.SweepExpiredTTL(ctx)
	if err != nil {
		t.Fatal(err)
	}
	if swept != 1 {
		t.Errorf("SweepExpiredTTL returned %d, want 1", swept)
	}
	var status string
	_ = q.st.DB().QueryRow(`SELECT status FROM messages WHERE id='m-ttl'`).Scan(&status)
	if status != "failed" {
		t.Errorf("expired message status = %q, want \"failed\"", status)
	}
}

// TestDeadLetterNotifiesSender is the regression test for violation C1: when
// a message exceeds MaxAttempts the original sender must learn about it.
// The brief's principle: "report failures to either the sender or a relevant
// agent (orchestrator)." Today a dead-lettered message was a silent floor-drop.
func TestDeadLetterNotifiesSender(t *testing.T) {
	q := newQ(t)
	q.MaxAttempts = 1
	q.BackoffBase = 1 * time.Millisecond
	q.BackoffMax = 1 * time.Millisecond
	ctx := context.Background()
	// Sender = "alice", recipient = "bob".
	msg := mkMsg("m1", "bob", 0)
	msg.From = "alice"
	msg.TaskID = "task-xyz"
	if err := q.Send(ctx, msg); err != nil {
		t.Fatal(err)
	}
	// One receive → attempts==1; then Nack pushes it to failed.
	if _, err := q.Receive(ctx, "bob"); err != nil {
		t.Fatal(err)
	}
	if err := q.Nack(ctx, "m1", "worker exploded"); err != nil {
		t.Fatal(err)
	}
	// The original sender (alice) must now have a TypeNack envelope in
	// their inbox referring to the dead message.
	notice, err := q.Receive(ctx, "alice")
	if err != nil {
		t.Fatal(err)
	}
	if notice == nil {
		t.Fatalf("sender 'alice' got no dead-letter notification — broken handoff chain")
	}
	if notice.Type != envelope.TypeNack {
		t.Errorf("dead-letter notice type = %s, want nack", notice.Type)
	}
	if notice.TaskID != "task-xyz" {
		t.Errorf("dead-letter notice task_id = %s, want task-xyz", notice.TaskID)
	}
	// Reason should propagate so the sender knows WHY.
	if notice.Payload.Reason == "" {
		t.Errorf("dead-letter notice has empty Reason — sender can't react meaningfully")
	}
}

func TestConcurrentDequeueNoDuplicate(t *testing.T) {
	q := newQ(t)
	ctx := context.Background()
	const N = 200
	for i := 0; i < N; i++ {
		id := "m" + itoa(i)
		if err := q.Send(ctx, mkMsg(id, "b", 0)); err != nil {
			t.Fatal(err)
		}
	}
	var wg sync.WaitGroup
	var got int64
	dups := make(map[string]int)
	var mu sync.Mutex
	for w := 0; w < 8; w++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for {
				e, err := q.Receive(ctx, "b")
				if err != nil {
					t.Errorf("recv: %v", err)
					return
				}
				if e == nil {
					return
				}
				mu.Lock()
				dups[e.ID]++
				mu.Unlock()
				atomic.AddInt64(&got, 1)
				_ = q.Ack(ctx, e.ID)
			}
		}()
	}
	wg.Wait()

	if got != int64(N) {
		t.Errorf("received %d, want %d", got, N)
	}
	for id, c := range dups {
		if c != 1 {
			t.Errorf("message %s delivered %d times", id, c)
		}
	}
}

func itoa(n int) string {
	if n == 0 {
		return "0"
	}
	neg := n < 0
	if neg {
		n = -n
	}
	var buf [20]byte
	i := len(buf)
	for n > 0 {
		i--
		buf[i] = byte('0' + n%10)
		n /= 10
	}
	if neg {
		i--
		buf[i] = '-'
	}
	return string(buf[i:])
}
