package clipboard

import (
	"context"
	"database/sql"
	"errors"
	"sync"
	"sync/atomic"
	"testing"
	"time"

	"commilitia.net/cdrop/internal/db"
	"commilitia.net/cdrop/internal/hub"
)

// fakeQuerier 在内存里模拟 clipboard_state 表，按 user_id 单条覆写。
type fakeQuerier struct {
	mu   sync.Mutex
	rows map[string]db.ClipboardState
	gets atomic.Int32
	puts atomic.Int32
}

func newFakeQuerier() *fakeQuerier {
	return &fakeQuerier{rows: map[string]db.ClipboardState{}}
}

func (f *fakeQuerier) UpsertClipboard(_ context.Context, arg db.UpsertClipboardParams) (int64, error) {
	f.puts.Add(1)
	f.mu.Lock()
	defer f.mu.Unlock()
	prev, exists := f.rows[arg.UserID]
	if exists && arg.OriginTs <= prev.OriginTs {
		return 0, sql.ErrNoRows // conditional WHERE failed → stale, no change (RETURNING no row)
	}
	version := prev.Version + 1 // absent → 0 → new version 1; else prev+1
	f.rows[arg.UserID] = db.ClipboardState{
		UserID:       arg.UserID,
		ContentType:  arg.ContentType,
		Content:      arg.Content,
		SourceDevice: arg.SourceDevice,
		UpdatedAt:    arg.UpdatedAt,
		Version:      version,
		OriginTs:     arg.OriginTs,
	}
	return version, nil
}

func (f *fakeQuerier) GetClipboard(_ context.Context, userID string) (db.ClipboardState, error) {
	f.gets.Add(1)
	f.mu.Lock()
	defer f.mu.Unlock()
	row, ok := f.rows[userID]
	if !ok {
		return db.ClipboardState{}, errNoRow
	}
	return row, nil
}

// ClearExpiredClipboards 模拟 SQL：把 content 非空且 updated_at < cutoff 的行
// 内容置 nil，返回清除行数。
func (f *fakeQuerier) ClearExpiredClipboards(_ context.Context, cutoff int64) (int64, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	var n int64
	for k, row := range f.rows {
		if row.Content != nil && row.UpdatedAt < cutoff {
			row.Content = nil
			row.SourceDevice = nil
			f.rows[k] = row
			n++
		}
	}
	return n, nil
}

var errNoRow = errors.New("no row")

// fakeHub 计数 Broadcast 调用并把 events 排队让测试断言。
type fakeHub struct {
	mu     sync.Mutex
	events []hub.Event
}

func (h *fakeHub) Broadcast(_ string, ev hub.Event) {
	h.mu.Lock()
	h.events = append(h.events, ev)
	h.mu.Unlock()
}

func (h *fakeHub) snapshot() []hub.Event {
	h.mu.Lock()
	defer h.mu.Unlock()
	out := make([]hub.Event, len(h.events))
	copy(out, h.events)
	return out
}

func TestPutWritesDBImmediately(t *testing.T) {
	q := newFakeQuerier()
	h := &fakeHub{}
	s := New(q, h, 1024, 50*time.Millisecond, 0)

	if _, _, err := s.Put(context.Background(), "alice", "text/plain", "hello", "tab-1", 1); err != nil {
		t.Fatalf("Put: %v", err)
	}
	if got := q.puts.Load(); got != 1 {
		t.Errorf("expected 1 DB upsert, got %d", got)
	}
	got, err := s.Get(context.Background(), "alice")
	if err != nil {
		t.Fatalf("Get: %v", err)
	}
	if got.Content != "hello" {
		t.Errorf("content: got %q, want %q", got.Content, "hello")
	}
}

func TestPutTrailingPutOnlyBroadcastsLast(t *testing.T) {
	q := newFakeQuerier()
	h := &fakeHub{}
	s := New(q, h, 1024, 60*time.Millisecond, 0)
	ctx := context.Background()

	for i, text := range []string{"a", "b", "c"} {
		if _, _, err := s.Put(ctx, "alice", "text/plain", text, "tab-1", int64(i+1)); err != nil {
			t.Fatalf("Put %q: %v", text, err)
		}
		time.Sleep(10 * time.Millisecond) // 每次写都在同窗口内
	}

	// 等待最后一次 Put 之后的窗口结束 + 余量
	time.Sleep(150 * time.Millisecond)

	evs := h.snapshot()
	if len(evs) != 1 {
		t.Fatalf("expected 1 broadcast, got %d", len(evs))
	}
	st, ok := evs[0].Data.(*State)
	if !ok {
		t.Fatalf("event data type: %T", evs[0].Data)
	}
	if st.Content != "c" {
		t.Errorf("broadcast content: got %q, want %q", st.Content, "c")
	}
	if evs[0].Type != EventType {
		t.Errorf("event type: got %q, want %q", evs[0].Type, EventType)
	}
}

func TestPutDifferentUsersDontInterfere(t *testing.T) {
	q := newFakeQuerier()
	h := &fakeHub{}
	s := New(q, h, 1024, 40*time.Millisecond, 0)
	ctx := context.Background()

	if _, _, err := s.Put(ctx, "alice", "text/plain", "alice-data", "tab-1", 1); err != nil {
		t.Fatalf("alice: %v", err)
	}
	if _, _, err := s.Put(ctx, "bob", "text/plain", "bob-data", "tab-1", 1); err != nil {
		t.Fatalf("bob: %v", err)
	}

	time.Sleep(120 * time.Millisecond)

	evs := h.snapshot()
	if len(evs) != 2 {
		t.Fatalf("expected 2 broadcasts (one per user), got %d", len(evs))
	}
}

func TestPutRejectsUnsupportedType(t *testing.T) {
	s := New(newFakeQuerier(), &fakeHub{}, 1024, 10*time.Millisecond, 0)
	_, _, err := s.Put(context.Background(), "alice", "text/html", "<b>hi</b>", "tab-1", 1)
	if !errors.Is(err, ErrUnsupportedType) {
		t.Errorf("expected ErrUnsupportedType, got %v", err)
	}
}

func TestPutRejectsTooLarge(t *testing.T) {
	s := New(newFakeQuerier(), &fakeHub{}, 8, 10*time.Millisecond, 0)
	_, _, err := s.Put(context.Background(), "alice", "text/plain", "123456789", "tab-1", 1)
	if !errors.Is(err, ErrTooLarge) {
		t.Errorf("expected ErrTooLarge, got %v", err)
	}
}

func TestPutEmptyContentAccepted(t *testing.T) {
	q := newFakeQuerier()
	h := &fakeHub{}
	s := New(q, h, 1024, 30*time.Millisecond, 0)

	if _, _, err := s.Put(context.Background(), "alice", "text/plain", "", "tab-1", 1); err != nil {
		t.Errorf("empty content should be accepted (clear semantics): %v", err)
	}
	time.Sleep(80 * time.Millisecond)
	evs := h.snapshot()
	if len(evs) != 1 {
		t.Fatalf("expected 1 broadcast, got %d", len(evs))
	}
	st := evs[0].Data.(*State)
	if st.Content != "" {
		t.Errorf("expected empty content, got %q", st.Content)
	}
}

func TestPutWindowExtendsOnNewWrite(t *testing.T) {
	q := newFakeQuerier()
	h := &fakeHub{}
	window := 80 * time.Millisecond
	s := New(q, h, 1024, window, 0)
	ctx := context.Background()

	t0 := time.Now()
	_, _, _ = s.Put(ctx, "alice", "text/plain", "first", "tab-1", 1)
	time.Sleep(window / 2)
	_, _, _ = s.Put(ctx, "alice", "text/plain", "second", "tab-1", 2)

	// 第一窗到期点：任何广播都不该出现（被第二个 Put 重置）
	time.Sleep(window/2 + 10*time.Millisecond)
	if got := len(h.snapshot()); got != 0 {
		t.Errorf("at t=%v, expected 0 broadcasts (window not yet elapsed for second), got %d",
			time.Since(t0), got)
	}

	// 第二窗到期之后 broadcast 应该出现，且内容是 "second"
	time.Sleep(window/2 + 30*time.Millisecond)
	evs := h.snapshot()
	if len(evs) != 1 {
		t.Fatalf("expected 1 broadcast after second window, got %d", len(evs))
	}
	if got := evs[0].Data.(*State).Content; got != "second" {
		t.Errorf("broadcast content: got %q, want %q", got, "second")
	}
}

// --- TTL（短存活）---

func seedRow(q *fakeQuerier, userID, content string, ageSec int64) {
	c := content
	src := "tab-1"
	q.rows[userID] = db.ClipboardState{
		UserID:       userID,
		ContentType:  "text/plain",
		Content:      &c,
		SourceDevice: &src,
		UpdatedAt:    time.Now().Unix() - ageSec,
	}
}

func TestGetExpiredReturnsEmpty(t *testing.T) {
	q := newFakeQuerier()
	seedRow(q, "alice", "secret", 100) // 100s 前写入
	s := New(q, &fakeHub{}, 1024, 10*time.Millisecond, 60*time.Second)

	got, err := s.Get(context.Background(), "alice")
	if err != nil {
		t.Fatalf("Get: %v", err)
	}
	if got.Content != "" {
		t.Errorf("过期内容不应回吐，got %q", got.Content)
	}
	if got.UpdatedAt != 0 {
		t.Errorf("过期状态 UpdatedAt 应为 0（等同未写入），got %d", got.UpdatedAt)
	}
}

func TestGetFreshWithinTTL(t *testing.T) {
	q := newFakeQuerier()
	seedRow(q, "alice", "recent", 5) // 5s 前
	s := New(q, &fakeHub{}, 1024, 10*time.Millisecond, 60*time.Second)

	got, err := s.Get(context.Background(), "alice")
	if err != nil {
		t.Fatalf("Get: %v", err)
	}
	if got.Content != "recent" {
		t.Errorf("TTL 内的新内容应返回，got %q", got.Content)
	}
}

func TestGetTTLDisabledNeverExpires(t *testing.T) {
	q := newFakeQuerier()
	seedRow(q, "alice", "old", 100000)
	s := New(q, &fakeHub{}, 1024, 10*time.Millisecond, 0) // ttl=0 → 禁用

	got, _ := s.Get(context.Background(), "alice")
	if got.Content != "old" {
		t.Errorf("ttl=0 永不过期，got %q", got.Content)
	}
}

func TestSweepExpiredClearsOldOnly(t *testing.T) {
	q := newFakeQuerier()
	seedRow(q, "alice", "old-secret", 100) // 过期
	seedRow(q, "bob", "recent", 5)         // 未过期
	s := New(q, &fakeHub{}, 1024, 10*time.Millisecond, 60*time.Second)

	n, err := s.SweepExpired(context.Background())
	if err != nil {
		t.Fatalf("SweepExpired: %v", err)
	}
	if n != 1 {
		t.Errorf("应清除 1 行（alice），got %d", n)
	}
	if c := q.rows["alice"].Content; c != nil {
		t.Errorf("alice 的内容应被清空（nil），got %v", *c)
	}
	if c := q.rows["bob"].Content; c == nil || *c != "recent" {
		t.Errorf("bob 的新内容应存活，got %v", c)
	}
}

func TestSweepExpiredNoopWhenDisabled(t *testing.T) {
	q := newFakeQuerier()
	seedRow(q, "alice", "x", 100000)
	s := New(q, &fakeHub{}, 1024, 10*time.Millisecond, 0)

	n, err := s.SweepExpired(context.Background())
	if err != nil || n != 0 {
		t.Errorf("ttl=0 时 SweepExpired 应为 noop，got n=%d err=%v", n, err)
	}
	if q.rows["alice"].Content == nil {
		t.Error("ttl=0 不应清除任何内容")
	}
}

func TestPutBumpsVersionMonotonically(t *testing.T) {
	svc := New(newFakeQuerier(), &fakeHub{}, 1024, time.Hour, 0)
	st1, ok, err := svc.Put(context.Background(), "u", ContentTypeText, "a", "devA", 1)
	if err != nil || !ok {
		t.Fatalf("put 1: ok=%v err=%v", ok, err)
	}
	if st1.Version != 1 {
		t.Errorf("first put version: got %d, want 1", st1.Version)
	}
	st2, ok, err := svc.Put(context.Background(), "u", ContentTypeText, "b", "devA", 2)
	if err != nil || !ok {
		t.Fatalf("put 2: ok=%v err=%v", ok, err)
	}
	if st2.Version != 2 {
		t.Errorf("second put version: got %d, want 2", st2.Version)
	}
	got, err := svc.Get(context.Background(), "u")
	if err != nil {
		t.Fatalf("get: %v", err)
	}
	if got.Version != 2 || got.SourceDevice != "devA" {
		t.Errorf("get: version=%d source=%q, want 2/devA", got.Version, got.SourceDevice)
	}
}

// LWW: a delayed/out-of-order write with an older-or-equal origin_ts must be
// rejected (accepted=false) and leave the current winner intact.
func TestPutRejectsStaleOrigin(t *testing.T) {
	svc := New(newFakeQuerier(), &fakeHub{}, 1024, time.Hour, 0)
	ctx := context.Background()

	st, ok, err := svc.Put(ctx, "u", ContentTypeText, "new", "devA", 200)
	if err != nil || !ok || st.Version != 1 {
		t.Fatalf("first put: ok=%v ver=%v err=%v", ok, st.Version, err)
	}
	// Older copy arriving late → rejected, current winner returned.
	cur, ok, err := svc.Put(ctx, "u", ContentTypeText, "stale", "devB", 100)
	if err != nil {
		t.Fatalf("stale put err: %v", err)
	}
	if ok {
		t.Error("older origin_ts must be rejected")
	}
	if cur.Content != "new" || cur.Version != 1 {
		t.Errorf("rejected put must return current winner; got content=%q ver=%d", cur.Content, cur.Version)
	}
	// Equal origin_ts is not strictly greater → rejected (idempotent duplicate).
	if _, ok, _ := svc.Put(ctx, "u", ContentTypeText, "dup", "devA", 200); ok {
		t.Error("equal origin_ts must be rejected (idempotent)")
	}
}
