package qryptinvite

import (
	"crypto/ed25519"
	"encoding/base64"
	"encoding/json"
	"strings"
	"testing"
	"time"
)

// verifyAsQrypt independently checks a token the way the qrypt.chat backend
// would: split into 3 segments, require segment[0] == "qci1", verify the
// Ed25519 signature over "qci1."+payloadSeg with the issuer's public key, and
// decode the payload. It deliberately does NOT reuse Mint's internals.
func verifyAsQrypt(t *testing.T, token string, pub ed25519.PublicKey) Payload {
	t.Helper()
	parts := strings.Split(token, ".")
	if len(parts) != 3 {
		t.Fatalf("token has %d segments, want 3", len(parts))
	}
	if parts[0] != "qci1" {
		t.Fatalf("segment[0] = %q, want qci1", parts[0])
	}
	signingInput := parts[0] + "." + parts[1]
	sig, err := base64.RawURLEncoding.DecodeString(parts[2])
	if err != nil {
		t.Fatalf("decode sig: %v", err)
	}
	if !ed25519.Verify(pub, []byte(signingInput), sig) {
		t.Fatal("signature did not verify")
	}
	pj, err := base64.RawURLEncoding.DecodeString(parts[1])
	if err != nil {
		t.Fatalf("decode payload: %v", err)
	}
	var p Payload
	if err := json.Unmarshal(pj, &p); err != nil {
		t.Fatalf("unmarshal payload: %v", err)
	}
	return p
}

func TestMintAndVerify(t *testing.T) {
	seedB64, pubB64, err := GenerateIssuerKey()
	if err != nil {
		t.Fatal(err)
	}
	priv, err := ParsePrivateKey(seedB64)
	if err != nil {
		t.Fatalf("ParsePrivateKey(seed): %v", err)
	}
	pubRaw, err := base64.StdEncoding.DecodeString(pubB64)
	if err != nil {
		t.Fatal(err)
	}
	pub := ed25519.PublicKey(pubRaw)

	before := time.Now()
	token, jti, err := Mint("agentbbs", priv, 168*time.Hour)
	if err != nil {
		t.Fatalf("Mint: %v", err)
	}

	p := verifyAsQrypt(t, token, pub)

	if p.Iss != "agentbbs" {
		t.Errorf("iss = %q, want agentbbs", p.Iss)
	}
	if p.Tier != "anonymous" {
		t.Errorf("tier = %q, want anonymous", p.Tier)
	}
	if p.Uses != 1 {
		t.Errorf("uses = %d, want 1", p.Uses)
	}
	if p.JTI != jti {
		t.Errorf("payload jti %q != returned jti %q", p.JTI, jti)
	}
	if len(p.JTI) != 32 {
		t.Errorf("jti len = %d, want 32 hex chars", len(p.JTI))
	}
	if p.Exp <= time.Now().Unix() {
		t.Errorf("exp %d is not in the future", p.Exp)
	}
	if p.Iat < before.Unix()-1 || p.Iat > time.Now().Unix()+1 {
		t.Errorf("iat %d outside the mint window", p.Iat)
	}
	// exp == iat + ttl
	if got, want := p.Exp-p.Iat, int64((168 * time.Hour).Seconds()); got != want {
		t.Errorf("exp-iat = %d, want %d", got, want)
	}
}

func TestJTIUnique(t *testing.T) {
	seedB64, _, err := GenerateIssuerKey()
	if err != nil {
		t.Fatal(err)
	}
	priv, err := ParsePrivateKey(seedB64)
	if err != nil {
		t.Fatal(err)
	}
	seen := map[string]bool{}
	for i := 0; i < 100; i++ {
		_, jti, err := Mint("agentbbs", priv, time.Hour)
		if err != nil {
			t.Fatal(err)
		}
		if seen[jti] {
			t.Fatalf("duplicate jti %q on iteration %d", jti, i)
		}
		seen[jti] = true
	}
}

func TestTamperedTokenFails(t *testing.T) {
	seedB64, pubB64, err := GenerateIssuerKey()
	if err != nil {
		t.Fatal(err)
	}
	priv, _ := ParsePrivateKey(seedB64)
	pubRaw, _ := base64.StdEncoding.DecodeString(pubB64)
	pub := ed25519.PublicKey(pubRaw)

	token, _, err := Mint("agentbbs", priv, time.Hour)
	if err != nil {
		t.Fatal(err)
	}
	parts := strings.Split(token, ".")

	// Tamper with the payload: flip the tier to "verified" and re-encode. The
	// signature was made over the original payload, so verification must fail.
	pj, _ := base64.RawURLEncoding.DecodeString(parts[1])
	var p Payload
	if err := json.Unmarshal(pj, &p); err != nil {
		t.Fatal(err)
	}
	p.Tier = "verified"
	p.Uses = 9999
	tj, _ := json.Marshal(p)
	tampered := parts[0] + "." + base64.RawURLEncoding.EncodeToString(tj) + "." + parts[2]

	if got := verifySig(tampered, pub); got {
		t.Fatal("tampered token verified but should have failed")
	}
	// The untouched token still verifies, proving the key is right.
	if !verifySig(token, pub) {
		t.Fatal("original token failed to verify")
	}

	// Tampering with the signature segment must also fail. Corrupt a DECODED
	// signature byte (not a base64 char): flipping the last base64 char can land
	// on the unused trailing bits of a 64-byte signature, which decode back to
	// the same bytes and still verify — that made this test flaky.
	sigBytes, err := base64.RawURLEncoding.DecodeString(parts[2])
	if err != nil || len(sigBytes) == 0 {
		t.Fatalf("decode signature segment: %v", err)
	}
	sigBytes[0] ^= 0xFF
	badSig := parts[0] + "." + parts[1] + "." + base64.RawURLEncoding.EncodeToString(sigBytes)
	if verifySig(badSig, pub) {
		t.Fatal("token with corrupted signature verified but should have failed")
	}
}

// verifySig is a minimal boolean form of the qrypt verify path.
func verifySig(token string, pub ed25519.PublicKey) bool {
	parts := strings.Split(token, ".")
	if len(parts) != 3 || parts[0] != "qci1" {
		return false
	}
	sig, err := base64.RawURLEncoding.DecodeString(parts[2])
	if err != nil {
		return false
	}
	return ed25519.Verify(pub, []byte(parts[0]+"."+parts[1]), sig)
}

func TestParsePrivateKeyAcceptsSeedAndFull(t *testing.T) {
	_, priv, err := ed25519.GenerateKey(nil)
	if err != nil {
		t.Fatal(err)
	}
	seedB64 := base64.StdEncoding.EncodeToString(priv.Seed())
	fullB64 := base64.StdEncoding.EncodeToString(priv)

	fromSeed, err := ParsePrivateKey(seedB64)
	if err != nil {
		t.Fatalf("seed: %v", err)
	}
	fromFull, err := ParsePrivateKey(fullB64)
	if err != nil {
		t.Fatalf("full: %v", err)
	}
	if !fromSeed.Equal(fromFull) {
		t.Fatal("seed and full-key parses produced different keys")
	}
	if !fromSeed.Equal(priv) {
		t.Fatal("parsed key differs from original")
	}

	if _, err := ParsePrivateKey("not-base64-@@@"); err == nil {
		t.Error("expected error on garbage input")
	}
	if _, err := ParsePrivateKey(base64.StdEncoding.EncodeToString([]byte("short"))); err == nil {
		t.Error("expected error on wrong-length key")
	}
}
