package innerauth

import (
	"context"
	"fmt"
	"testing"
	"time"

	"github.com/mem9-ai/drive9/pkg/tenant"
)

func TestDispatcherAPIKeyCachesExpire(t *testing.T) {
	account := &fakeAccountProvider{apiKeyAuthnResult: APIKeyAuthnResult{PublicKey: "public", OrganizationID: 60205}}
	dispatcher := NewDispatcher(account, &fakeDefaultCredentials{credentials: tenant.CredentialProvisionRequest{PublicKey: "shared-public", PrivateKey: "shared-private"}, ok: true}, time.Hour)
	now := time.Unix(100, 0)
	dispatcher.apiKeyCache.now = func() time.Time { return now }
	caller := tenant.CredentialProvisionRequest{PublicKey: "public", PrivateKey: "private"}
	subject := ControlPlaneSubject{Kind: CredentialAPIKey, PublicKey: "public"}

	if _, err := dispatcher.Authenticate(context.Background(), "", 0, caller); err != nil {
		t.Fatalf("initial Authenticate: %v", err)
	}
	if err := dispatcher.Authorize(context.Background(), subject, "/request", "/resource"); err != nil {
		t.Fatalf("initial Authorize: %v", err)
	}
	now = now.Add(time.Hour + time.Nanosecond)
	if _, err := dispatcher.Authenticate(context.Background(), "", 0, caller); err != nil {
		t.Fatalf("expired Authenticate: %v", err)
	}
	if err := dispatcher.Authorize(context.Background(), subject, "/request", "/resource"); err != nil {
		t.Fatalf("expired Authorize: %v", err)
	}
	if account.apiKeyAuthnCalls != 2 || account.apiKeyAuthzCalls != 2 {
		t.Fatalf("API key calls = (authn %d, authz %d), want (2, 2)", account.apiKeyAuthnCalls, account.apiKeyAuthzCalls)
	}
}

func TestDispatcherCachesModifyAuthorizationUntilConfiguredTTLExpires(t *testing.T) {
	const cacheTTL = 30 * time.Minute
	account := &fakeAccountProvider{}
	dispatcher := NewDispatcher(account, nil, cacheTTL)
	now := time.Unix(100, 0)
	dispatcher.apiKeyCache.now = func() time.Time { return now }
	subject := ControlPlaneSubject{Kind: CredentialAPIKey, PublicKey: "public"}
	requestURI := "/drive9/orgs/60205/admin/tenants/modify"
	resourceURI := "/drive9/orgs/{org_id}/admin/tenants/modify"

	if err := dispatcher.Authorize(context.Background(), subject, requestURI, resourceURI); err != nil {
		t.Fatalf("initial Authorize: %v", err)
	}
	now = now.Add(cacheTTL - time.Nanosecond)
	if err := dispatcher.Authorize(context.Background(), subject, requestURI, resourceURI); err != nil {
		t.Fatalf("cached Authorize: %v", err)
	}
	if account.apiKeyAuthzCalls != 1 {
		t.Fatalf("APIKeyAuthz calls before TTL = %d, want 1", account.apiKeyAuthzCalls)
	}
	now = now.Add(2 * time.Nanosecond)
	if err := dispatcher.Authorize(context.Background(), subject, requestURI, resourceURI); err != nil {
		t.Fatalf("expired Authorize: %v", err)
	}
	if account.apiKeyAuthzCalls != 2 {
		t.Fatalf("APIKeyAuthz calls after TTL = %d, want 2", account.apiKeyAuthzCalls)
	}
}

func TestAPIKeyCacheBoundsAuthenticationAndAuthorizationEntries(t *testing.T) {
	cache := newAPIKeyCache(time.Hour)
	for i := 0; i <= apiKeyCacheMaxEntries; i++ {
		value := fmt.Sprintf("entry-%d", i)
		cache.rememberAuthn(value, APIKeyAuthnResult{PublicKey: value, OrganizationID: uint64(i + 1)})
		cache.rememberAuthz(value, "/request/"+value, "/resource")
	}
	if len(cache.authn) != apiKeyCacheMaxEntries || len(cache.authz) != apiKeyCacheMaxEntries {
		t.Fatalf("cache entries = (authn %d, authz %d), want (%d, %d)", len(cache.authn), len(cache.authz), apiKeyCacheMaxEntries, apiKeyCacheMaxEntries)
	}
}

func TestAPIKeyCacheEvictsLeastRecentlyUsedAuthenticationEntry(t *testing.T) {
	cache := newAPIKeyCache(time.Hour)
	for i := 0; i < apiKeyCacheMaxEntries; i++ {
		value := fmt.Sprintf("entry-%d", i)
		cache.rememberAuthn(value, APIKeyAuthnResult{PublicKey: value, OrganizationID: uint64(i + 1)})
	}

	if _, ok := cache.getAuthn("entry-0"); !ok {
		t.Fatal("authentication cache did not contain touched entry")
	}
	cache.rememberAuthn("new-entry", APIKeyAuthnResult{PublicKey: "new-entry", OrganizationID: 99999})

	if _, ok := cache.getAuthn("entry-1"); ok {
		t.Fatal("least recently used authentication entry survived overflow")
	}
	for _, key := range []string{"entry-0", "new-entry"} {
		if _, ok := cache.getAuthn(key); !ok {
			t.Fatalf("authentication cache lost %q", key)
		}
	}
}

func TestAPIKeyCacheEvictsLeastRecentlyUsedAuthorizationEntry(t *testing.T) {
	cache := newAPIKeyCache(time.Hour)
	for i := 0; i < apiKeyCacheMaxEntries; i++ {
		value := fmt.Sprintf("entry-%d", i)
		cache.rememberAuthz(value, "/request/"+value, "/resource")
	}

	if !cache.hasAuthz("entry-0", "/request/entry-0", "/resource") {
		t.Fatal("authorization cache did not contain touched entry")
	}
	cache.rememberAuthz("new-entry", "/request/new-entry", "/resource")

	if cache.hasAuthz("entry-1", "/request/entry-1", "/resource") {
		t.Fatal("least recently used authorization entry survived overflow")
	}
	for _, key := range []string{"entry-0", "new-entry"} {
		if !cache.hasAuthz(key, "/request/"+key, "/resource") {
			t.Fatalf("authorization cache lost %q", key)
		}
	}
}

func TestDispatcherValidatesCallerPublicKeyOnAuthenticationCacheHit(t *testing.T) {
	account := &fakeAccountProvider{}
	dispatcher := NewDispatcher(account, nil, time.Hour)
	caller := tenant.CredentialProvisionRequest{PublicKey: "caller-public", PrivateKey: "caller-private"}
	encoded, err := EncodeAPIKey(caller)
	if err != nil {
		t.Fatalf("EncodeAPIKey: %v", err)
	}
	dispatcher.apiKeyCache.rememberAuthn(encoded, APIKeyAuthnResult{PublicKey: "different-public", OrganizationID: 60205})

	if _, err := dispatcher.Authenticate(context.Background(), "", 0, caller); err != ErrInvalidResponse {
		t.Fatalf("Authenticate error = %v, want %v", err, ErrInvalidResponse)
	}
	if account.apiKeyAuthnCalls != 0 {
		t.Fatalf("APIKeyAuthn calls = %d, want cache hit", account.apiKeyAuthnCalls)
	}
}
