package spindle

import (
	"context"
	"crypto/ed25519"
	"crypto/rand"
	"fmt"
	"strings"
	"testing"

	"github.com/bluesky-social/indigo/atproto/identity"
	"github.com/bluesky-social/indigo/atproto/syntax"
	gossh "golang.org/x/crypto/ssh"

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

type debugKeyEnv struct {
	*ensureEnv
}

func newDebugKeyEnv(t *testing.T, collaborators ...syntax.DID) *debugKeyEnv {
	t.Helper()
	env := newEnsureEnv(t)
	env.serve("repo", collaborators...)
	env.ensure(t)
	dir := env.s.res.Directory().(*identity.MockDirectory)
	for _, did := range append([]syntax.DID{testStranger}, collaborators...) {
		dir.Insert(identity.Identity{
			DID: did,
			Services: map[string]identity.ServiceEndpoint{
				"atproto_pds": {Type: "AtprotoPersonalDataServer", URL: env.knot.srv.URL},
			},
		})
	}
	return &debugKeyEnv{env}
}

const testStranger = syntax.DID("did:plc:stranger")

func newSSHKey(t *testing.T) gossh.PublicKey {
	t.Helper()
	pub, _, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		t.Fatal(err)
	}
	key, err := gossh.NewPublicKey(pub)
	if err != nil {
		t.Fatal(err)
	}
	return key
}

func (e *debugKeyEnv) giveKey(who syntax.DID, rkey string, key gossh.PublicKey, usages ...string) {
	e.knot.put(who, org_tangled.KeySshKeyNSID, rkey, org_tangled.KeySshKey{
		LexiconTypeID: org_tangled.KeySshKeyNSID,
		Name:          rkey,
		CreatedAt:     "2026-01-01T00:00:00Z",
		PublicKey:     strings.TrimSpace(string(gossh.MarshalAuthorizedKey(key))) + " laptop",
		Usages:        usages,
	})
}

func (e *debugKeyEnv) allowed(key gossh.PublicKey) bool {
	return e.s.DebugKeyAllowed(context.Background(), testRepoDid, key)
}

func TestDebugKeyOfTheOwnerGetsIn(t *testing.T) {
	e := newDebugKeyEnv(t)
	key := newSSHKey(t)
	e.giveKey(testOwner, "k1", key)
	if !e.allowed(key) {
		t.Fatal("the owner's key was refused")
	}
}

func TestDebugKeyOfACollaboratorGetsIn(t *testing.T) {
	e := newDebugKeyEnv(t, testBob)
	key := newSSHKey(t)
	e.giveKey(testBob, "k1", key)
	if !e.allowed(key) {
		t.Fatal("a collaborator's key was refused")
	}
}

func TestDebugKeyOfAStrangerStaysOut(t *testing.T) {
	e := newDebugKeyEnv(t, testBob)
	key := newSSHKey(t)
	e.giveKey(testStranger, "k1", key)
	e.giveKey(testBob, "k1", newSSHKey(t))
	if e.allowed(key) {
		t.Fatal("a key of someone without push access got in")
	}
}

func TestDebugKeyWhoseRecordIsGoneStaysOut(t *testing.T) {
	e := newDebugKeyEnv(t)
	key := newSSHKey(t)
	e.giveKey(testOwner, "k1", key)
	e.knot.erase(testOwner, org_tangled.KeySshKeyNSID, "k1")
	if e.allowed(key) {
		t.Fatal("a key whose record was deleted got in")
	}
}

func TestDebugKeyOfABannedCollaboratorStaysOut(t *testing.T) {
	e := newDebugKeyEnv(t, testBob)
	key := newSSHKey(t)
	e.giveKey(testBob, "k1", key)
	if err := e.s.db.PutBan(db.BanEntry{SubjectDid: testBob}); err != nil {
		t.Fatal(err)
	}
	if e.allowed(key) {
		t.Fatal("a banned collaborator's key got in")
	}
}

func TestDebugKeyOfAPausedOwnersRepoStaysOut(t *testing.T) {
	e := newDebugKeyEnv(t)
	key := newSSHKey(t)
	e.giveKey(testOwner, "k1", key)
	e.s.cfg.Server.Admission = config.AdmissionClosed
	if e.allowed(key) {
		t.Fatal("a key got in on a repo whose owner isn't admitted")
	}
}

func TestDebugKeyIsRefusedWhenThePDSErrors(t *testing.T) {
	e := newDebugKeyEnv(t, testBob)
	key := newSSHKey(t)
	e.giveKey(testOwner, "k1", key)
	e.knot.failCollection = org_tangled.KeySshKeyNSID
	if e.allowed(key) {
		t.Fatal("a key got in while its holder's pds was failing")
	}
}

func TestDebugKeyMatchesByTheMarshalledKeyNotTheComment(t *testing.T) {
	e := newDebugKeyEnv(t)
	key := newSSHKey(t)
	e.giveKey(testOwner, "k1", key)
	// a different comment and whitespace around the same key is still the same key
	e.knot.put(testOwner, org_tangled.KeySshKeyNSID, "k1", org_tangled.KeySshKey{
		Name:      "k1",
		CreatedAt: "2026-01-01T00:00:00Z",
		PublicKey: "  " + strings.TrimSpace(string(gossh.MarshalAuthorizedKey(key))) + " other comment \n",
	})
	if !e.allowed(key) {
		t.Fatal("the same key under another comment was refused")
	}
}

func TestDebugKeyThatOnlySignsIsNotAnAuthKey(t *testing.T) {
	e := newDebugKeyEnv(t)
	key := newSSHKey(t)
	e.giveKey(testOwner, "k1", key, "sign")
	if e.allowed(key) {
		t.Fatal("a signing-only key got in")
	}
}

func TestDebugKeysAreCachedPerDidForAMoment(t *testing.T) {
	e := newDebugKeyEnv(t)
	key := newSSHKey(t)
	e.giveKey(testOwner, "k1", key)
	if !e.allowed(key) {
		t.Fatal("the owner's key was refused")
	}
	before := e.knot.count()
	for range 5 {
		if !e.allowed(key) {
			t.Fatal("the owner's key was refused on a repeat")
		}
	}
	if got := e.knot.count(); got != before {
		t.Fatalf("%d pds requests for repeats inside the cache window, want none", got-before)
	}
}

func TestDebugKeyLookupStopsAfterAFewHolders(t *testing.T) {
	var collaborators []syntax.DID
	for i := range 3 * debugKeyHolderCap {
		collaborators = append(collaborators, syntax.DID(fmt.Sprintf("did:plc:collab%03d", i)))
	}
	e := newDebugKeyEnv(t, collaborators...)
	before := e.knot.count()
	if e.allowed(newSSHKey(t)) {
		t.Fatal("a key nobody holds got in")
	}
	if got := e.knot.count() - before; got > debugKeyHolderCap {
		t.Fatalf("%d pds requests for one check, want at most %d", got, debugKeyHolderCap)
	}
}
