package credentiallease

import (
	"context"
	"crypto/ecdh"
	"crypto/ed25519"
	"crypto/rand"
	"errors"
	"strings"
	"testing"
	"time"

	"google.golang.org/protobuf/proto"
)

func testKeys(t *testing.T) (ed25519.PublicKey, ed25519.PrivateKey, []byte, []byte) {
	t.Helper()
	issuerPublic, issuerPrivate, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		t.Fatal(err)
	}
	recipient, err := ecdh.X25519().GenerateKey(rand.Reader)
	if err != nil {
		t.Fatal(err)
	}
	return issuerPublic, issuerPrivate, recipient.Bytes(), recipient.PublicKey().Bytes()
}

func testScope(now time.Time, id string) Scope {
	return Scope{
		LeaseID: id, PrincipalRef: "principal-1", CredentialSlotRef: "slot-1", RouteID: "route-1",
		ProfileID: "openai", UpstreamTarget: "gpt-4o", NodeID: "node-1", RecipientKeyID: "recipient-1",
		HeaderName: "Authorization", Scheme: "Bearer", CredentialRevision: 7, RouteRevision: 5,
		ProjectionGeneration: 11, IssuedAtUnixNano: now.UnixNano(), ExpiresAtUnixNano: now.Add(30 * time.Second).UnixNano(),
	}
}

func expected(scope Scope) ExpectedScope {
	return ExpectedScope{
		PrincipalRef: scope.PrincipalRef, CredentialSlotRef: scope.CredentialSlotRef, RouteID: scope.RouteID,
		ProfileID: scope.ProfileID, UpstreamTarget: scope.UpstreamTarget, NodeID: scope.NodeID,
		RecipientKeyID: scope.RecipientKeyID, CredentialRevision: scope.CredentialRevision,
		RouteRevision: scope.RouteRevision, ProjectionGeneration: scope.ProjectionGeneration,
	}
}

func TestIssueConsumeExactScopeAndProtoRedaction(t *testing.T) {
	now := time.Unix(1700000000, 0).UTC()
	issuerPublic, issuerPrivate, recipientPrivate, recipientPublic := testKeys(t)
	secret := []byte("SENTINEL_PROVIDER_SECRET")
	env, err := Issue(testScope(now, "lease-1"), secret, recipientPublic, "issuer-1", issuerPrivate, rand.Reader)
	if err != nil {
		t.Fatal(err)
	}
	if strings.Contains(env.ToProto().String(), string(secret)) {
		t.Fatal("plaintext secret appeared in protobuf debug output")
	}
	wire, err := proto.Marshal(env.ToProto())
	if err != nil {
		t.Fatal(err)
	}
	if strings.Contains(string(wire), string(secret)) {
		t.Fatal("plaintext secret appeared in protobuf wire bytes")
	}
	consumer, err := NewConsumer("node-1", "recipient-1", recipientPrivate, "issuer-1", issuerPublic, 8, func() time.Time { return now })
	if err != nil {
		t.Fatal(err)
	}
	material, err := consumer.Consume(context.Background(), env, expected(env.Scope))
	if err != nil {
		t.Fatal(err)
	}
	if got := string(material.Secret); got != string(secret) || material.HeaderName != "Authorization" || material.Scheme != "Bearer" {
		t.Fatalf("unexpected material metadata")
	}
	material.Zero()
	if material.Secret != nil {
		t.Fatal("material was not released")
	}
	if _, err := consumer.Consume(context.Background(), env, expected(env.Scope)); !errors.Is(err, ErrReplay) {
		t.Fatalf("duplicate consume error = %v, want replay", err)
	}
}

func TestScopeTamperAndExpectedMismatchFailBeforePlaintext(t *testing.T) {
	now := time.Unix(1700000000, 0).UTC()
	issuerPublic, issuerPrivate, recipientPrivate, recipientPublic := testKeys(t)
	base, err := Issue(testScope(now, "lease-tamper"), []byte("secret"), recipientPublic, "issuer-1", issuerPrivate, rand.Reader)
	if err != nil {
		t.Fatal(err)
	}
	fields := []func(*Scope){
		func(s *Scope) { s.PrincipalRef += "x" }, func(s *Scope) { s.CredentialSlotRef += "x" },
		func(s *Scope) { s.RouteID += "x" }, func(s *Scope) { s.ProfileID += "x" },
		func(s *Scope) { s.UpstreamTarget += "x" }, func(s *Scope) { s.NodeID += "x" },
		func(s *Scope) { s.RecipientKeyID += "x" }, func(s *Scope) { s.CredentialRevision++ },
		func(s *Scope) { s.RouteRevision++ }, func(s *Scope) { s.ProjectionGeneration++ },
		func(s *Scope) { s.ExpiresAtUnixNano++ },
	}
	for i, mutate := range fields {
		copyEnvelope := *base
		mutate(&copyEnvelope.Scope)
		consumer, _ := NewConsumer("node-1", "recipient-1", recipientPrivate, "issuer-1", issuerPublic, 8, func() time.Time { return now })
		if material, err := consumer.Consume(context.Background(), &copyEnvelope, expected(copyEnvelope.Scope)); err == nil || material != nil {
			t.Fatalf("tamper case %d was accepted", i)
		}
	}
	consumer, _ := NewConsumer("node-1", "recipient-1", recipientPrivate, "issuer-1", issuerPublic, 8, func() time.Time { return now })
	mismatch := expected(base.Scope)
	mismatch.RouteRevision++
	if material, err := consumer.Consume(context.Background(), base, mismatch); !errors.Is(err, ErrScopeMismatch) || material != nil {
		t.Fatalf("expected scope mismatch, got material=%v err=%v", material, err)
	}
}

func TestTTLReplayBoundsOverflowAndExpiryPruning(t *testing.T) {
	now := time.Unix(1700000000, 0).UTC()
	_, issuerPrivate, _, recipientPublic := testKeys(t)
	for _, ttl := range []time.Duration{MinTTL - time.Nanosecond, MaxTTL + time.Nanosecond} {
		scope := testScope(now, "invalid-ttl")
		scope.ExpiresAtUnixNano = now.Add(ttl).UnixNano()
		if _, err := Issue(scope, []byte("secret"), recipientPublic, "issuer", issuerPrivate, rand.Reader); !errors.Is(err, ErrInvalid) {
			t.Fatalf("ttl %s error = %v", ttl, err)
		}
	}
	issuerPublic, issuerPrivate, recipientPrivate, recipientPublic := testKeys(t)
	clock := now.Add(time.Second)
	firstScope := testScope(now, "lease-first")
	firstScope.ExpiresAtUnixNano = now.Add(MinTTL).UnixNano()
	secondScope := testScope(now.Add(time.Second), "lease-second")
	secondScope.ExpiresAtUnixNano = now.Add(time.Second + MinTTL).UnixNano()
	first, _ := Issue(firstScope, []byte("one"), recipientPublic, "issuer", issuerPrivate, rand.Reader)
	second, _ := Issue(secondScope, []byte("two"), recipientPublic, "issuer", issuerPrivate, rand.Reader)
	consumer, err := NewConsumer("node-1", "recipient-1", recipientPrivate, "issuer", issuerPublic, 1, func() time.Time { return clock })
	if err != nil {
		t.Fatal(err)
	}
	material, err := consumer.Consume(context.Background(), first, expected(first.Scope))
	if err != nil {
		t.Fatal(err)
	}
	material.Zero()
	if _, err := consumer.Consume(context.Background(), second, expected(second.Scope)); !errors.Is(err, ErrOverflow) {
		t.Fatalf("live overflow error = %v", err)
	}
	clock = now.Add(MinTTL + time.Nanosecond)
	material, err = consumer.Consume(context.Background(), second, expected(second.Scope))
	if err != nil {
		t.Fatalf("consume after expired prune: %v", err)
	}
	material.Zero()
	if _, err := NewConsumer("node", "key", recipientPrivate, "issuer", issuerPublic, 0, nil); !errors.Is(err, ErrInvalid) {
		t.Fatalf("zero replay bound error = %v", err)
	}
	if _, err := NewConsumer("node", "key", recipientPrivate, "issuer", issuerPublic, MaxSetSize+1, nil); !errors.Is(err, ErrInvalid) {
		t.Fatalf("oversize replay bound error = %v", err)
	}
}
