package spindle

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

	"github.com/bluesky-social/indigo/atproto/syntax"

	"tangled.org/core/spindle/db"
	"tangled.org/core/spindle/secrets"
)

type flakyVault struct {
	secrets.Manager
	fails atomic.Int32
}

func (f *flakyVault) RemoveAllSecrets(ctx context.Context, repo secrets.RepoIdentifier) error {
	if f.fails.Add(-1) >= 0 {
		return errors.New("vault is down")
	}
	return f.Manager.RemoveAllSecrets(ctx, repo)
}

func wantWipeFinished(t *testing.T, s *Spindle, repo syntax.DID) {
	t.Helper()
	if _, err := s.db.GetWipe(repo); !errors.Is(err, sql.ErrNoRows) {
		t.Fatalf("the wipe didn't finish: %v", err)
	}
	if got := secretCount(t, s, repo); got != 0 {
		t.Fatalf("secrets = %d after the wipe, want none", got)
	}
}

func TestAFailedWipeStepIsRetriedUntilItIsDone(t *testing.T) {
	s := newWipeTestSpindle(t)
	ctx := context.Background()
	repoDid, owner := syntax.DID("did:plc:repo1"), syntax.DID("did:plc:owner1")
	seedWipeRepo(t, s, repoDid, owner)
	vault := &flakyVault{Manager: s.vault}
	vault.fails.Store(1)
	s.vault = vault

	if err := s.WipeRepo(ctx, repoDid, "test"); err == nil {
		t.Fatal("a wipe whose secrets step failed reported success")
	}
	w, err := s.db.GetWipe(repoDid)
	if err != nil {
		t.Fatalf("no wipe left to retry: %v", err)
	}
	if !slices.Equal(w.Steps, []string{"secrets"}) || w.Attempts != 1 || !w.Due.After(time.Now()) {
		t.Fatalf("wipe = %+v, want only the secrets step left, one failure and a backoff", w)
	}
	if rows := repoRows(t, s, repoDid); len(rows) != 0 {
		t.Fatalf("the repo is still served mid-wipe: %+v", rows)
	}
	if got := secretCount(t, s, repoDid); got != 1 {
		t.Fatalf("secrets = %d before the retry, want them still there", got)
	}

	s.retries.now = func() time.Time { return time.Now().Add(2 * time.Hour) }
	s.runDueWipe(ctx, repoDid)
	wantWipeFinished(t, s, repoDid)
}

func TestAWipeThatWasClaimedFinishesAfterARestart(t *testing.T) {
	s := newWipeTestSpindle(t)
	repoDid, owner := syntax.DID("did:plc:repo1"), syntax.DID("did:plc:owner1")
	seedWipeRepo(t, s, repoDid, owner)

	if _, err := s.claimWipeIfRows(context.Background(), repoDid, "test"); err != nil {
		t.Fatal(err)
	}

	s.kickDueRepos()
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	did, ok := s.retries.jobs.next(ctx)
	if !ok || did != repoDid {
		t.Fatalf("the restart queued %q, %v, want the repo with the unfinished wipe", did, ok)
	}
	s.runRepoJob(context.Background(), did)
	wantWipeFinished(t, s, repoDid)
}

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

	cleanup, err := env.s.claimWipeIfRows(context.Background(), testRepoDid, "test")
	if err != nil || cleanup == nil {
		t.Fatalf("claim left no cleanup to run, err %v", err)
	}
	// the knot still names this spindle, but the unfinished wipe keeps the repo out
	repo, err := env.s.ensureRepo(context.Background(), env.k, testRepoDid)
	if err != nil || repo != nil {
		t.Fatalf("a refresh with a wipe unfinished = %+v, %v, want the repo left unserved", repo, err)
	}
	if err := cleanup(); err != nil {
		t.Fatal(err)
	}
	if env.secrets(t) != 0 {
		t.Fatal("the wipe left the repo's secrets")
	}
	if repo := env.ensure(t); repo == nil {
		t.Fatal("a refresh after the wipe is done can't register the repo")
	}
	if env.secrets(t) != 0 {
		t.Fatal("registering the repo again brought its secrets back")
	}
}

func TestABanTakesTheReposOfItsSubjectThroughTheWipeTable(t *testing.T) {
	s := newWipeTestSpindle(t)
	repoDid, owner := syntax.DID("did:plc:repo1"), syntax.DID("did:plc:owner1")
	seedWipeRepo(t, s, repoDid, owner)
	if err := s.db.PutBan(db.BanEntry{SubjectDid: owner}); err != nil {
		t.Fatal(err)
	}
	if err := s.db.PutBan(db.BanEntry{SubjectDid: "did:plc:ownsnothing"}); err != nil {
		t.Fatal(err)
	}

	s.resumeWipes(context.Background())

	if rows := repoRows(t, s, repoDid); len(rows) != 0 {
		t.Fatalf("a banned owner's repo is still served: %+v", rows)
	}
	w, err := s.db.GetWipe(repoDid)
	if err != nil || w.Reason != "banned" {
		t.Fatalf("wipe = %+v, %v, want one for the repo with the ban as its reason", w, err)
	}
	if exists, err := s.db.WipeExists("did:plc:ownsnothing"); err != nil || exists {
		t.Fatalf("a ban for a did with no repos made a wipe, exists %v, %v", exists, err)
	}

	s.runRepoJob(context.Background(), repoDid)
	wantWipeFinished(t, s, repoDid)
}
