package db

import (
	"testing"
	"time"
)

// AddExecutorToken must fail closed on a name collision: a second registration
// under a live identity's name cannot silently take it over, and the failed
// insert must not leave the newcomer's token resolvable.
func TestAddExecutorTokenRejectsDuplicateName(t *testing.T) {
	d := newTestDB(t)

	if err := d.AddExecutorToken("exec-1", "hash-a", nil); err != nil {
		t.Fatalf("AddExecutorToken: %v", err)
	}
	if err := d.AddExecutorToken("exec-1", "hash-b", nil); err == nil {
		t.Fatal("AddExecutorToken re-registered an existing name; a duplicate must be rejected")
	}

	// the original identity is untouched...
	name, ok, err := d.ResolveExecutorToken("hash-a")
	if err != nil {
		t.Fatalf("ResolveExecutorToken(hash-a): %v", err)
	}
	if !ok || name != "exec-1" {
		t.Fatalf("ResolveExecutorToken(hash-a) = (%q, %v), want (exec-1, true)", name, ok)
	}
	// ...and the rejected duplicate's token never became a credential.
	if _, ok, _ := d.ResolveExecutorToken("hash-b"); ok {
		t.Fatal("rejected duplicate's token resolved; the failed insert leaked a credential")
	}
}

// ResolveExecutorToken is the auth lookup: an unknown hash must fail closed
// (ok=false), a known hash must resolve to the exact identity it was
// registered under.
func TestResolveExecutorTokenMissAndHit(t *testing.T) {
	d := newTestDB(t)

	// miss on an empty table: unknown token does not authenticate.
	name, ok, err := d.ResolveExecutorToken("no-such-hash")
	if err != nil {
		t.Fatalf("ResolveExecutorToken(miss): %v", err)
	}
	if ok || name != "" {
		t.Fatalf("ResolveExecutorToken(miss) = (%q, %v), want (\"\", false)", name, ok)
	}

	if err := d.AddExecutorToken("exec-1", "hash-1", nil); err != nil {
		t.Fatalf("AddExecutorToken: %v", err)
	}

	// hit: resolves to the registered identity.
	name, ok, err = d.ResolveExecutorToken("hash-1")
	if err != nil {
		t.Fatalf("ResolveExecutorToken(hit): %v", err)
	}
	if !ok || name != "exec-1" {
		t.Fatalf("ResolveExecutorToken(hash-1) = (%q, %v), want (exec-1, true)", name, ok)
	}

	// a hash no identity was registered under still misses.
	if _, ok, _ := d.ResolveExecutorToken("hash-unregistered"); ok {
		t.Fatal("ResolveExecutorToken matched an unregistered hash")
	}
}

// UpsertExecutorToken rotates an identity's credential: after an upsert the new
// hash authenticates as the same name and the rotated-out hash no longer does.
// An upsert of a fresh name inserts it.
func TestUpsertExecutorTokenRotatesHash(t *testing.T) {
	d := newTestDB(t)

	if err := d.AddExecutorToken("exec-1", "old-hash", nil); err != nil {
		t.Fatalf("AddExecutorToken: %v", err)
	}
	if err := d.UpsertExecutorToken("exec-1", "new-hash"); err != nil {
		t.Fatalf("UpsertExecutorToken(rotate): %v", err)
	}

	name, ok, err := d.ResolveExecutorToken("new-hash")
	if err != nil {
		t.Fatalf("ResolveExecutorToken(new-hash): %v", err)
	}
	if !ok || name != "exec-1" {
		t.Fatalf("ResolveExecutorToken(new-hash) = (%q, %v), want (exec-1, true)", name, ok)
	}
	if _, ok, _ := d.ResolveExecutorToken("old-hash"); ok {
		t.Fatal("rotated-out token still resolves; UpsertExecutorToken did not replace the hash")
	}

	// upsert of a name that does not exist yet inserts a new identity.
	if err := d.UpsertExecutorToken("exec-2", "hash-2"); err != nil {
		t.Fatalf("UpsertExecutorToken(insert): %v", err)
	}
	name, ok, err = d.ResolveExecutorToken("hash-2")
	if err != nil {
		t.Fatalf("ResolveExecutorToken(hash-2): %v", err)
	}
	if !ok || name != "exec-2" {
		t.Fatalf("ResolveExecutorToken(hash-2) = (%q, %v), want (exec-2, true)", name, ok)
	}
}

// RevokeExecutorToken removes an identity so its token stops authenticating,
// and reports deleted=false when the name is not present.
func TestRevokeExecutorToken(t *testing.T) {
	d := newTestDB(t)

	if err := d.AddExecutorToken("exec-1", "hash-1", nil); err != nil {
		t.Fatalf("AddExecutorToken: %v", err)
	}

	deleted, err := d.RevokeExecutorToken("exec-1")
	if err != nil {
		t.Fatalf("RevokeExecutorToken: %v", err)
	}
	if !deleted {
		t.Fatal("RevokeExecutorToken reported no deletion for an existing identity")
	}

	// the revoked token must no longer authenticate.
	if _, ok, _ := d.ResolveExecutorToken("hash-1"); ok {
		t.Fatal("revoked token still resolves; revocation is not enforced")
	}

	// revoking the now-absent name reports nothing deleted.
	if deleted, err := d.RevokeExecutorToken("exec-1"); err != nil || deleted {
		t.Fatalf("RevokeExecutorToken(already-gone) = (%v, %v), want (false, nil)", deleted, err)
	}

	// revoking a name that never existed reports nothing deleted.
	if deleted, err := d.RevokeExecutorToken("ghost"); err != nil || deleted {
		t.Fatalf("RevokeExecutorToken(unknown) = (%v, %v), want (false, nil)", deleted, err)
	}
}

// ListExecutorTokens returns every registered identity ordered by name.
func TestListExecutorTokens(t *testing.T) {
	d := newTestDB(t)

	tokens, err := d.ListExecutorTokens()
	if err != nil {
		t.Fatalf("ListExecutorTokens(empty): %v", err)
	}
	if len(tokens) != 0 {
		t.Fatalf("ListExecutorTokens on empty table = %d rows, want 0", len(tokens))
	}

	// insert out of alphabetical order to prove the ordering is the query's.
	for _, name := range []string{"charlie", "alice", "bob"} {
		if err := d.AddExecutorToken(name, "hash-"+name, nil); err != nil {
			t.Fatalf("AddExecutorToken(%s): %v", name, err)
		}
	}

	tokens, err = d.ListExecutorTokens()
	if err != nil {
		t.Fatalf("ListExecutorTokens: %v", err)
	}
	want := []string{"alice", "bob", "charlie"}
	if len(tokens) != len(want) {
		t.Fatalf("ListExecutorTokens = %d rows, want %d", len(tokens), len(want))
	}
	for i := range want {
		if tokens[i].Name != want[i] {
			t.Fatalf("ListExecutorTokens[%d].Name = %q, want %q (ordered by name)", i, tokens[i].Name, want[i])
		}
	}
}

// ResolveExecutorToken enforces token expiry fail-closed: a token whose
// expires_at lies in the past must NOT authenticate (ok=false, no error),
// while a future or absent expiry authenticates as normal. An expired
// credential that still resolves is an auth defect, so this pins that branch.
func TestResolveExecutorTokenExpiry(t *testing.T) {
	cases := []struct {
		name     string
		offset   time.Duration
		noExpiry bool
		wantOK   bool
	}{
		{"future expiry resolves", time.Hour, false, true},
		{"past expiry fails closed", -time.Hour, false, false},
		{"nil expiry never expires", 0, true, true},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			d := newTestDB(t)

			var expires *time.Time
			if !tc.noExpiry {
				exp := time.Now().Add(tc.offset)
				expires = &exp
			}
			if err := d.AddExecutorToken("exec-1", "hash-1", expires); err != nil {
				t.Fatalf("AddExecutorToken: %v", err)
			}

			name, ok, err := d.ResolveExecutorToken("hash-1")
			if err != nil {
				t.Fatalf("ResolveExecutorToken: %v", err)
			}
			if ok != tc.wantOK {
				t.Fatalf("ResolveExecutorToken ok = %v, want %v", ok, tc.wantOK)
			}
			if tc.wantOK && name != "exec-1" {
				t.Fatalf("ResolveExecutorToken name = %q, want exec-1", name)
			}
			if !tc.wantOK && name != "" {
				t.Fatalf("ResolveExecutorToken name = %q, want \"\" when failing closed", name)
			}
		})
	}
}

// ListExecutorTokens surfaces each identity's expiry: nil for a non-expiring
// token and the stored instant for an expiring one. Storage is RFC3339
// second-precision UTC, so the round-tripped instant must equal the original
// to the second.
func TestListExecutorTokensSurfacesExpiry(t *testing.T) {
	d := newTestDB(t)

	exp := time.Now().Add(24 * time.Hour)
	if err := d.AddExecutorToken("expiring", "hash-exp", &exp); err != nil {
		t.Fatalf("AddExecutorToken(expiring): %v", err)
	}
	if err := d.AddExecutorToken("forever", "hash-forever", nil); err != nil {
		t.Fatalf("AddExecutorToken(forever): %v", err)
	}

	tokens, err := d.ListExecutorTokens()
	if err != nil {
		t.Fatalf("ListExecutorTokens: %v", err)
	}

	got := make(map[string]*time.Time, len(tokens))
	for _, tok := range tokens {
		got[tok.Name] = tok.ExpiresAt
	}

	// the non-expiring token reports a nil expiry.
	e, present := got["forever"]
	if !present {
		t.Fatal("ListExecutorTokens omitted the non-expiring identity")
	}
	if e != nil {
		t.Fatalf("forever.ExpiresAt = %v, want nil (never expires)", e)
	}

	// the expiring token reports its stored instant, to the second.
	e, present = got["expiring"]
	if !present {
		t.Fatal("ListExecutorTokens omitted the expiring identity")
	}
	if e == nil {
		t.Fatal("expiring.ExpiresAt = nil, want the stored expiry")
	}
	if e.Unix() != exp.Unix() {
		t.Fatalf("expiring.ExpiresAt = %d (unix), want %d", e.Unix(), exp.Unix())
	}
}
