package spindle

import (
	"context"
	"fmt"
	"net/http"
	"sync"
	"testing"

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

	"tangled.org/core/api/org_tangled"
	"tangled.org/core/jetstream"
)

func (e *ensureEnv) markCollaboratorEvent(t *testing.T, who syntax.DID, op string) {
	t.Helper()
	evt, ok := jetstreamToTapEvent(collaboratorCommit(testRepoDid, who, op))
	if !ok {
		t.Fatalf("no tap event for %s", op)
	}
	if err := e.s.processEvent(withMarkOnly(context.Background()), evt); err != nil {
		t.Fatal(err)
	}
}

func TestAPastTheCapRepoStillTakesAGrantAndARevokeOneRecordAtATime(t *testing.T) {
	env := newEnsureEnv(t)
	env.serve("repo", testBob)
	env.ensure(t)
	env.wantPush(t, testBob, true)

	// 3300 is one page past what the knot client reads, which means a sync can't finish in one go
	for i := range 3300 {
		env.grant(syntax.DID(fmt.Sprintf("did:plc:bulk%d", i)))
	}

	env.revoke(testBob)
	env.markCollaboratorEvent(t, testBob, jetstream.CommitOperationDelete)
	env.grant(testCarol)
	env.markCollaboratorEvent(t, testCarol, jetstream.CommitOperationCreate)

	before := env.knot.count()
	env.s.runRepoJob(context.Background(), testRepoDid)

	env.wantNoPushFor(t, testBob)
	env.wantPush(t, testCarol, true)
	if got := env.knot.count() - before; got > 40 {
		t.Fatalf("the job made %d knot requests, one failed sync is about 35 and each record is one more", got)
	}
	if got := dueRecords(t, env); len(got) != 0 {
		t.Fatalf("collaborator marks left behind: %+v", got)
	}
	if _, marked, err := env.s.db.RepoRetryRow(testRepoDid); err != nil || marked {
		t.Fatalf("a repo that can't be synced whole keeps retrying, exists %v, %v", marked, err)
	}
}

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

	env.grant(testCarol)
	env.markCollaboratorEvent(t, testCarol, jetstream.CommitOperationCreate)
	before := env.knot.count()
	env.s.runRepoJob(context.Background(), testRepoDid)

	env.wantPush(t, testCarol, true)
	if got := env.knot.count() - before; got > 3 {
		t.Fatalf("the job made %d knot requests, a refresh is 3 and the collaborator record needs no read of its own", got)
	}
	if got := dueRecords(t, env); len(got) != 0 {
		t.Fatalf("the full sync left collaborator marks: %+v", got)
	}
}

func TestACollaboratorMarkAppliedAfterAWipeDoesNotGrantAccessToTheWipedRepo(t *testing.T) {
	env := newEnsureEnv(t)
	env.serve("repo")
	env.ensure(t)
	env.grant(testBob)
	env.markCollaboratorEvent(t, testBob, jetstream.CommitOperationCreate)

	// the wipe lands during the mark's knot lookup, fired by the first collaborator query
	var wiped sync.Once
	env.knot.beforeAnswer = func(r *http.Request) {
		if r.URL.Query().Get("collection") != org_tangled.GraphCollaboratorNSID {
			return
		}
		wiped.Do(func() {
			if err := env.s.WipeRepo(context.Background(), testRepoDid, "test"); err != nil {
				t.Errorf("WipeRepo: %v", err)
			}
		})
	}
	env.s.runRecordMarks(context.Background(), testRepoDid)

	if rows, err := env.s.db.ListCollaboratorsByRepoDid(testRepoDid); err != nil || len(rows) != 0 {
		t.Fatalf("collaborator rows after the wipe = %+v, %v, want none", rows, err)
	}
	env.wantNoPushFor(t, testBob)
}
