package spindle

import (
	"testing"
	"time"

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

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

const pausedOwner = syntax.DID("did:plc:thespindleowner")

// enroll runs before seed, a repo only gets served if its owner is already admitted
func newClosedPushEnv(t *testing.T, asAdmin bool, enroll func(*db.DB) error) *pushEnv {
	t.Helper()
	w := newWorld(t)
	w.admission = config.AdmissionClosed
	if !asAdmin {
		w.owner = pausedOwner
	}
	repo := w.serveGitRepo(testRepoDid)
	n := w.boot()
	if enroll != nil {
		if err := enroll(n.s.db); err != nil {
			t.Fatal(err)
		}
		// the allowlist xrpc tracks the owner too, or hydrant never backfills them
		n.s.backfill.Track(testOwner)
	}
	w.seed(n, testBob)
	return &pushEnv{w: w, n: n, repo: repo}
}

func listOwner(d *db.DB) error { return db.AddToAllowlist(d, testOwner) }

func inviteOwner(d *db.DB) error {
	code, _, err := d.MintInvite("did:plc:someadmin", time.Now())
	if err != nil {
		return err
	}
	return d.RedeemInvite(code, testOwner, time.Now())
}

func (e *pushEnv) allow(t *testing.T) {
	t.Helper()
	if err := db.AddToAllowlist(e.n.s.db, testOwner); err != nil {
		t.Fatal(err)
	}
}

func (e *pushEnv) disallow(t *testing.T) {
	t.Helper()
	if err := db.RemoveFromAllowlist(e.n.s.db, testOwner); err != nil {
		t.Fatal(err)
	}
}

func (e *pushEnv) pushNext(t *testing.T) string {
	t.Helper()
	uri, err := e.notify(e.n.s, e.firstPush(t))
	if err != nil {
		t.Fatalf("notify: %v", err)
	}
	return string(uri)
}

func TestRemovedFromTheAllowlistPausesTheOwnersRepos(t *testing.T) {
	e := newClosedPushEnv(t, false, listOwner)
	addTestSecret(t, e.n.s, testRepoDid)

	if e.pushNext(t) == "" {
		t.Fatal("setup: an admitted owner's push made no pipeline")
	}
	e.disallow(t)
	if uri := e.pushNext(t); uri != "" {
		t.Fatalf("a push of an owner who was removed from the allow-list made pipeline %q", uri)
	}
	e.wantPipelines(t, 1, "after the owner was removed")
	if got := secretCount(t, e.n.s, testRepoDid); got != 1 {
		t.Fatalf("%d secrets after the owner was removed, want the 1 that was there", got)
	}
	if len(repoRows(t, e.n.s, testRepoDid)) != 1 {
		t.Fatal("the repo row went away with the allow-list entry")
	}

	e.allow(t)
	if e.pushNext(t) == "" {
		t.Fatal("a push made no pipeline after the owner was added back")
	}
	e.wantPipelines(t, 2, "after the owner was added back")
}

func TestOwnerEnrolledByInviteIsPausedAndResumedToo(t *testing.T) {
	e := newClosedPushEnv(t, false, inviteOwner)
	if e.pushNext(t) == "" {
		t.Fatal("setup: an owner enrolled by invite made no pipeline")
	}

	e.disallow(t)
	if uri := e.pushNext(t); uri != "" {
		t.Fatalf("a push of an invited owner who was removed made pipeline %q", uri)
	}
	e.allow(t)
	if e.pushNext(t) == "" {
		t.Fatal("a push made no pipeline after the invited owner was added back")
	}
}

func TestAdminOwnerIsNeverPaused(t *testing.T) {
	e := newClosedPushEnv(t, true, nil)
	if e.pushNext(t) == "" {
		t.Fatal("an admin owner who isn't on the allow-list made no pipeline under closed admission")
	}
}

func TestOpeningAdmissionResumesAPausedOwner(t *testing.T) {
	e := newClosedPushEnv(t, false, listOwner)
	e.disallow(t)
	if uri := e.pushNext(t); uri != "" {
		t.Fatalf("an owner who isn't admitted made pipeline %q", uri)
	}

	// admission is read at boot, opening it takes a restart
	e.n.stop()
	e.w.admission = config.AdmissionOpen
	e.n = e.w.boot()
	if e.pushNext(t) == "" {
		t.Fatal("a push made no pipeline after admission opened")
	}
}
