package spindle

import (
	"context"
	"sync"
	"testing"
	"time"

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

type gatedVault struct {
	secrets.Manager
	entered chan struct{}
	gate    chan struct{}
	enter   sync.Once
	release sync.Once
}

func newGatedVault(inner secrets.Manager) *gatedVault {
	return &gatedVault{Manager: inner, entered: make(chan struct{}), gate: make(chan struct{})}
}

func (g *gatedVault) RemoveAllSecrets(ctx context.Context, repo secrets.RepoIdentifier) error {
	g.enter.Do(func() { close(g.entered) })
	<-g.gate
	return g.Manager.RemoveAllSecrets(ctx, repo)
}

func (g *gatedVault) open() { g.release.Do(func() { close(g.gate) }) }

func (g *gatedVault) waitEntered(t *testing.T) {
	t.Helper()
	select {
	case <-g.entered:
	case <-time.After(5 * time.Second):
		t.Fatal("the wipe never reached its cleanup")
	}
}

func TestAWipeFromARefreshHoldsNoLockWhileItsCleanupRuns(t *testing.T) {
	env := newEnsureEnv(t)
	env.serve("repo")
	env.ensure(t)
	vault := newGatedVault(env.s.vault)
	env.s.vault = vault
	defer vault.open()

	env.knot.deleteRepo(testRepoDid)
	done := ensureAsync(env)
	vault.waitEntered(t)

	locked := make(chan struct{})
	go func() {
		defer env.s.lockRepo(testRepoDid).Unlock()
		close(locked)
	}()
	select {
	case <-locked:
	case <-time.After(2 * time.Second):
		t.Fatal("the repo's lock was held while the wipe cleaned up")
	}
	if repo, err := env.s.servedRepo(testRepoDid); err != nil || repo != nil {
		t.Errorf("the repo is still served while its cleanup runs: %+v, %v", repo, err)
	}

	vault.open()
	if err := <-done; err != nil {
		t.Fatalf("ensureRepo: %v", err)
	}
}

func TestAWipeStopsTheReposWritersBeforeItsCleanup(t *testing.T) {
	env := newEnsureEnv(t)
	env.serve("repo")
	repo := env.ensure(t)
	vault := newGatedVault(env.s.vault)
	env.s.vault = vault
	defer vault.open()

	done := goErr(func() error { return env.s.WipeRepo(context.Background(), testRepoDid, "test") })
	vault.waitEntered(t)

	wrote := false
	if err := env.s.whileServed(repo, func() error {
		wrote = true
		return nil
	}); err != nil {
		t.Fatal(err)
	}
	if wrote {
		t.Error("a write landed for a repo whose wipe was cleaning up")
	}
	if rows := env.rows(t); len(rows) != 0 {
		t.Errorf("the repo's rows outlive the start of its cleanup: %+v", rows)
	}

	vault.open()
	if err := <-done; err != nil {
		t.Fatalf("WipeRepo: %v", err)
	}
}

func TestARefreshWhileAWipeCleansUpDoesNotRegisterTheRepoAgain(t *testing.T) {
	env := newEnsureEnv(t)
	env.serve("repo")
	env.ensure(t)
	vault := newGatedVault(env.s.vault)
	env.s.vault = vault
	defer vault.open()

	done := goErr(func() error { return env.s.WipeRepo(context.Background(), testRepoDid, "test") })
	vault.waitEntered(t)

	repo, err := env.s.ensureRepo(context.Background(), env.k, testRepoDid)
	if err != nil || repo != nil {
		t.Fatalf("a refresh during the cleanup = %+v, %v, want the repo left unserved", repo, err)
	}
	if rows := env.rows(t); len(rows) != 0 {
		t.Fatalf("a refresh registered the repo again mid-cleanup: %+v", rows)
	}

	vault.open()
	if err := <-done; err != nil {
		t.Fatalf("WipeRepo: %v", err)
	}
	if repo := env.ensure(t); repo == nil {
		t.Fatal("a refresh after the wipe is over can't register the repo")
	}
}
