package keys

import (
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"path/filepath"
	"testing"

	comatproto "github.com/bluesky-social/indigo/api/atproto"
	"github.com/bluesky-social/indigo/atproto/identity"
	"github.com/bluesky-social/indigo/atproto/syntax"
	lexutil "github.com/bluesky-social/indigo/lex/util"
	"tangled.org/core/api/tangled"
	"tangled.org/core/knotserver/db"
)

const (
	didBoltless = "did:plc:boltless"
	keyAlpha    = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICrWubjgc3IM/zqjpWQJSig6l6iFyaDx7HWTiWlasjcM"
	keyBravo    = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMax5MnG4RGoxp0TlaEj8mcFhZZp13cdIIvO8s4a6KZ2"
)

func TestFetchAndStore_EmptyResponseDoesNotWipe(t *testing.T) {
	store := newKeyStore(t)
	seedKey(t, store, didBoltless, "seed", keyAlpha)

	srv := pdsServer(t, map[string]*comatproto.RepoListRecords_Output{
		"": page(""),
	})
	defer srv.Close()

	if err := FetchAndStore(context.Background(), fakeDirectory{pdsURL: srv.URL}, store, didBoltless); err != nil {
		t.Fatalf("FetchAndStore: %v", err)
	}

	owners := ownersByKey(t, store)
	if _, ok := owners[keyAlpha]; !ok {
		t.Fatalf("seeded key was wiped by an empty fetch response, owners=%v", owners)
	}
	if got := len(owners); got != 1 {
		t.Errorf("stored %d keys, want 1", got)
	}
}

func TestFetchAndStore_ReplacesExistingKeys(t *testing.T) {
	store := newKeyStore(t)
	seedKey(t, store, didBoltless, "stale", keyAlpha)

	srv := pdsServer(t, map[string]*comatproto.RepoListRecords_Output{
		"": page("", pubkeyRecord(didBoltless, "fresh", keyBravo)),
	})
	defer srv.Close()

	if err := FetchAndStore(context.Background(), fakeDirectory{pdsURL: srv.URL}, store, didBoltless); err != nil {
		t.Fatalf("FetchAndStore: %v", err)
	}

	owners := ownersByKey(t, store)
	if _, ok := owners[keyAlpha]; ok {
		t.Errorf("stale key %q survived a full replace", keyAlpha)
	}
	if _, ok := owners[keyBravo]; !ok {
		t.Errorf("fresh key %q was not stored", keyBravo)
	}
	if got := len(owners); got != 1 {
		t.Errorf("stored %d keys, want 1", got)
	}
}

func TestFetchAndStore_PaginatesAcrossPages(t *testing.T) {
	store := newKeyStore(t)
	if err := db.AddDid(store, didBoltless); err != nil {
		t.Fatalf("AddDid: %v", err)
	}

	srv := pdsServer(t, map[string]*comatproto.RepoListRecords_Output{
		"":     page("next", pubkeyRecord(didBoltless, "r1", keyAlpha)),
		"next": page("", pubkeyRecord(didBoltless, "r2", keyBravo)),
	})
	defer srv.Close()

	if err := FetchAndStore(context.Background(), fakeDirectory{pdsURL: srv.URL}, store, didBoltless); err != nil {
		t.Fatalf("FetchAndStore: %v", err)
	}

	owners := ownersByKey(t, store)
	if _, ok := owners[keyAlpha]; !ok {
		t.Errorf("first-page key %q missing", keyAlpha)
	}
	if _, ok := owners[keyBravo]; !ok {
		t.Errorf("second-page key %q missing", keyBravo)
	}
	if got := len(owners); got != 2 {
		t.Errorf("stored %d keys, want 2", got)
	}
}

func newKeyStore(t *testing.T) *db.DB {
	t.Helper()
	store, err := db.Setup(context.Background(), filepath.Join(t.TempDir(), "test.db"))
	if err != nil {
		t.Fatalf("db.Setup: %v", err)
	}
	return store
}

func seedKey(t *testing.T, store *db.DB, did syntax.DID, rkey syntax.RecordKey, key string) {
	t.Helper()
	if err := db.AddDid(store, did.String()); err != nil {
		t.Fatalf("AddDid: %v", err)
	}
	if err := store.UpsertPublicKey(db.PublicKey{
		Did:       did,
		Rkey:      rkey,
		PublicKey: tangled.PublicKey{Key: key, CreatedAt: "2026-06-20T00:00:00Z"},
	}); err != nil {
		t.Fatalf("UpsertPublicKey: %v", err)
	}
}

func ownersByKey(t *testing.T, store *db.DB) map[string]string {
	t.Helper()
	rows, err := store.GetAllPublicKeys()
	if err != nil {
		t.Fatalf("GetAllPublicKeys: %v", err)
	}
	return foldOwners(rows, map[string]string{})
}

func foldOwners(rows []db.PublicKey, acc map[string]string) map[string]string {
	if len(rows) == 0 {
		return acc
	}
	acc[rows[0].Key] = rows[0].Did.String()
	return foldOwners(rows[1:], acc)
}

func page(cursor string, records ...*comatproto.RepoListRecords_Record) *comatproto.RepoListRecords_Output {
	out := &comatproto.RepoListRecords_Output{Records: records}
	if cursor != "" {
		out.Cursor = &cursor
	}
	return out
}

func pubkeyRecord(did, rkey, key string) *comatproto.RepoListRecords_Record {
	return &comatproto.RepoListRecords_Record{
		Uri: "at://" + did + "/" + tangled.PublicKeyNSID + "/" + rkey,
		Cid: "bafyreib2rxk3rybk3aobmv5cjuql3bm2twh4jo5uxgr3rxbd3xrnp5z5cy",
		Value: &lexutil.LexiconTypeDecoder{
			Val: &tangled.PublicKey{Key: key, CreatedAt: "2026-06-20T00:00:00Z"},
		},
	}
}

func pdsServer(t *testing.T, pages map[string]*comatproto.RepoListRecords_Output) *httptest.Server {
	t.Helper()
	return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		cursor := r.URL.Query().Get("cursor")
		out, ok := pages[cursor]
		if !ok {
			t.Errorf("listRecords requested with unexpected cursor %q", cursor)
			http.Error(w, "no such page", http.StatusInternalServerError)
			return
		}
		w.Header().Set("Content-Type", "application/json")
		if err := json.NewEncoder(w).Encode(out); err != nil {
			t.Errorf("encoding listRecords response: %v", err)
		}
	}))
}

type fakeDirectory struct {
	pdsURL string
}

func (f fakeDirectory) LookupDID(ctx context.Context, did syntax.DID) (*identity.Identity, error) {
	return &identity.Identity{
		DID: did,
		Services: map[string]identity.ServiceEndpoint{
			"atproto_pds": {Type: "AtprotoPersonalDataServer", URL: f.pdsURL},
		},
	}, nil
}

func (f fakeDirectory) LookupHandle(ctx context.Context, handle syntax.Handle) (*identity.Identity, error) {
	return nil, errors.New("LookupHandle unused in tests")
}

func (f fakeDirectory) Lookup(ctx context.Context, atid syntax.AtIdentifier) (*identity.Identity, error) {
	return nil, errors.New("Lookup unused in tests")
}

func (f fakeDirectory) Purge(ctx context.Context, atid syntax.AtIdentifier) error {
	return nil
}
