package spindle

import (
	"context"
	"slices"
	"testing"
	"time"

	"github.com/bluesky-social/indigo/atproto/syntax"
	"tangled.org/core/api/org_tangled"
	"tangled.org/core/spindle/db"
	"tangled.org/core/tapc"
)

func pendingRetries(t *testing.T, d *db.DB) []syntax.DID {
	t.Helper()
	due, err := d.DueRepoRetries(time.Now().Add(24 * time.Hour))
	if err != nil {
		t.Fatal(err)
	}
	return due
}

func TestRepoRetriesBackOffAndGiveUp(t *testing.T) {
	d, _ := newTestSpindleDB(t)
	now := time.Unix(1000, 0)
	r := &repoRetries{now: func() time.Time { return now }}
	dueNow := func() []syntax.DID {
		t.Helper()
		due, err := d.DueRepoRetries(r.clock())
		if err != nil {
			t.Fatal(err)
		}
		return due
	}

	r.fail(d, testRepoDid)
	if got := dueNow(); len(got) != 0 {
		t.Fatalf("due right after failing: %v", got)
	}
	now = now.Add(repoRetryBase)
	if got := dueNow(); len(got) != 1 || got[0] != testRepoDid {
		t.Fatalf("due after the first backoff = %v", got)
	}

	r.fail(d, testRepoDid)
	now = now.Add(repoRetryBase)
	if got := dueNow(); len(got) != 0 {
		t.Fatalf("second failure should wait twice as long, due = %v", got)
	}

	dropped := false
	for range repoRetryAttempts {
		if retained, _ := r.fail(d, testRepoDid); !retained {
			dropped = true
			break
		}
	}
	if left := pendingRetries(t, d); !dropped || len(left) != 0 {
		t.Fatalf("a repo that keeps failing is never dropped, pending = %v", left)
	}
}

func TestRepoRetriesSurviveARestart(t *testing.T) {
	d, _ := newTestSpindleDB(t)
	now := time.Unix(1000, 0)
	r := &repoRetries{now: func() time.Time { return now }}
	r.fail(d, testRepoDid)
	r.fail(d, testRepoDid)

	now = now.Add(2 * repoRetryMax)
	restarted := &repoRetries{now: func() time.Time { return now }}
	if due, err := d.DueRepoRetries(restarted.clock()); err != nil || len(due) != 1 || due[0] != testRepoDid {
		t.Fatalf("due after a restart = %v, %v, want the repo that was failing", due, err)
	}
	restarted.fail(d, testRepoDid)
	if attempts, err := d.RepoRetryAttempts(testRepoDid); err != nil || attempts != 3 {
		t.Fatalf("attempts after a restart = %d, %v, want the count to carry on at 3", attempts, err)
	}
}

func TestAFailedLiveRefreshIsRetriedUntilItWorks(t *testing.T) {
	env := newEnsureEnv(t)
	env.serve("repo")
	env.ensure(t)

	env.knot.failing = true
	evt := recordEvent(testRepoDid, org_tangled.RepoManifestNSID, "self", tapc.RecordUpdateAction, org_tangled.RepoManifest{})
	if err := env.s.processEvent(context.Background(), evt); err == nil {
		t.Fatal("processEvent hid a knot failure")
	}
	if left := pendingRetries(t, env.s.db); !slices.Equal(left, []syntax.DID{testRepoDid}) {
		t.Fatalf("retry set = %v, want the repo whose refresh failed", left)
	}

	env.knot.failing = false
	if err := env.s.RefreshRepo(context.Background(), testRepoDid); err != nil {
		t.Fatal(err)
	}
	if left := pendingRetries(t, env.s.db); len(left) != 0 {
		t.Fatalf("a successful refresh left %v pending", left)
	}
}

func TestAFailedBackfillRefreshIsNotRetriedHere(t *testing.T) {
	env := newEnsureEnv(t)
	env.serve("repo")
	env.ensure(t)

	env.knot.failing = true
	evt := recordEvent(testRepoDid, org_tangled.RepoManifestNSID, "self", tapc.RecordUpdateAction, org_tangled.RepoManifest{})
	evt.Record.Live = false
	if err := env.s.processEvent(context.Background(), evt); err == nil {
		t.Fatal("processEvent hid a knot failure")
	}
	if left := pendingRetries(t, env.s.db); len(left) != 0 {
		t.Fatalf("backfill parks its own failures, pending = %v", left)
	}
}
