package ai

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"sync"
	"sync/atomic"
	"testing"
	"time"
)

const testFingerprintSecret = "model-cache-test-fingerprint-secret-32-bytes"

type fakeModelsCache struct {
	mu      sync.Mutex
	values  map[ModelsCacheKey]ModelsCacheValue
	locks   map[ModelsCacheKey]string
	seen    []ModelsCacheKey
	getErr  error
	lockErr error
}

func newFakeModelsCache() *fakeModelsCache {
	return &fakeModelsCache{values: make(map[ModelsCacheKey]ModelsCacheValue), locks: make(map[ModelsCacheKey]string)}
}

func (c *fakeModelsCache) Get(_ context.Context, key ModelsCacheKey) (ModelsCacheValue, bool, error) {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.seen = append(c.seen, key)
	if c.getErr != nil {
		return ModelsCacheValue{}, false, c.getErr
	}
	value, ok := c.values[key]
	value.Models = cloneModels(value.Models)
	return value, ok, nil
}

func (c *fakeModelsCache) Set(_ context.Context, key ModelsCacheKey, value ModelsCacheValue, _ time.Duration) error {
	c.mu.Lock()
	defer c.mu.Unlock()
	value.Models = cloneModels(value.Models)
	c.values[key] = value
	return nil
}

func (c *fakeModelsCache) AcquireRefresh(_ context.Context, key ModelsCacheKey, token string, _ time.Duration) (bool, error) {
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.lockErr != nil {
		return false, c.lockErr
	}
	if _, exists := c.locks[key]; exists {
		return false, nil
	}
	c.locks[key] = token
	return true, nil
}

func (c *fakeModelsCache) ReleaseRefresh(_ context.Context, key ModelsCacheKey, token string) error {
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.locks[key] == token {
		delete(c.locks, key)
	}
	return nil
}

type modelClient struct {
	calls   atomic.Int32
	models  []string
	err     error
	started chan struct{}
	release chan struct{}
}

func (c *modelClient) Complete(context.Context, string, string, string) (string, error) {
	return "", nil
}

func (c *modelClient) CompleteStream(context.Context, string, string, string, func(string) error) (string, error) {
	return "", nil
}

func (c *modelClient) ListModels(context.Context, string) ([]string, error) {
	c.calls.Add(1)
	if c.started != nil {
		select {
		case c.started <- struct{}{}:
		default:
		}
	}
	if c.release != nil {
		<-c.release
	}
	return cloneModels(c.models), c.err
}

func TestRegistryListModelsCacheHit(t *testing.T) {
	cache := newFakeModelsCache()
	client := &modelClient{models: []string{"provider-model"}}
	registry := NewRegistryWithClients(cache, testFingerprintSecret, map[string]Client{ProviderXAI: client})
	cache.values[registry.cacheKey(ProviderXAI, "api-key")] = ModelsCacheValue{
		Models: []string{"cached-model"}, FetchedAt: time.Now().UTC().UnixNano(),
	}

	models, fromCache, err := registry.ListModelsCached(context.Background(), ProviderXAI, "api-key")
	if err != nil || !fromCache || fmt.Sprint(models) != "[cached-model]" {
		t.Fatalf("unexpected result models=%v fromCache=%v err=%v", models, fromCache, err)
	}
	if client.calls.Load() != 0 {
		t.Fatalf("provider called on cache hit")
	}
}

func TestRegistrySeparatesProvidersAndCredentials(t *testing.T) {
	cache := newFakeModelsCache()
	xai := &modelClient{models: []string{"xai-model"}}
	opencode := &modelClient{models: []string{"opencode-model"}}
	registry := NewRegistryWithClients(cache, testFingerprintSecret, map[string]Client{
		ProviderXAI: xai, ProviderOpenCodeGo: opencode,
	})

	for _, call := range []struct{ provider, key string }{
		{ProviderXAI, "key-a"}, {ProviderXAI, "key-b"}, {ProviderOpenCodeGo, "key-a"},
	} {
		if _, _, err := registry.ListModelsCached(context.Background(), call.provider, call.key); err != nil {
			t.Fatal(err)
		}
	}

	cache.mu.Lock()
	entries := len(cache.values)
	cache.mu.Unlock()
	if entries != 3 || xai.calls.Load() != 2 || opencode.calls.Load() != 1 {
		t.Fatalf("separation failed entries=%d xai=%d opencode=%d", entries, xai.calls.Load(), opencode.calls.Load())
	}
}

func TestRegistryReturnsStaleOnProviderFailure(t *testing.T) {
	cache := newFakeModelsCache()
	client := &modelClient{err: errors.New("provider unavailable")}
	registry := NewRegistryWithClients(cache, testFingerprintSecret, map[string]Client{ProviderXAI: client})
	cache.values[registry.cacheKey(ProviderXAI, "api-key")] = ModelsCacheValue{
		Models: []string{"stale-model"}, FetchedAt: time.Now().UTC().Add(-10 * time.Minute).UnixNano(),
	}

	models, fromCache, err := registry.ListModelsCached(context.Background(), ProviderXAI, "api-key")
	if err != nil || !fromCache || fmt.Sprint(models) != "[stale-model]" {
		t.Fatalf("unexpected stale result models=%v fromCache=%v err=%v", models, fromCache, err)
	}
}

func TestRegistryRedisFailureFallsOpenToProvider(t *testing.T) {
	cache := newFakeModelsCache()
	cache.getErr = errors.New("redis read failed")
	cache.lockErr = errors.New("redis lock failed")
	client := &modelClient{models: []string{"provider-model"}}
	registry := NewRegistryWithClients(cache, testFingerprintSecret, map[string]Client{ProviderXAI: client})

	models, fromCache, err := registry.ListModelsCached(context.Background(), ProviderXAI, "api-key")
	if err != nil || fromCache || fmt.Sprint(models) != "[provider-model]" {
		t.Fatalf("unexpected fail-open result models=%v fromCache=%v err=%v", models, fromCache, err)
	}
	if client.calls.Load() != 1 {
		t.Fatalf("provider calls=%d, want 1", client.calls.Load())
	}
}

func TestRegistryCoalescesConcurrentRefresh(t *testing.T) {
	cache := newFakeModelsCache()
	client := &modelClient{
		models: []string{"fresh-model"}, started: make(chan struct{}, 1), release: make(chan struct{}),
	}
	registry := NewRegistryWithClients(cache, testFingerprintSecret, map[string]Client{ProviderXAI: client})
	type result struct {
		models []string
		err    error
	}
	results := make(chan result, 2)
	call := func() {
		models, _, err := registry.ListModelsCached(context.Background(), ProviderXAI, "api-key")
		results <- result{models: models, err: err}
	}
	go call()
	select {
	case <-client.started:
	case <-time.After(time.Second):
		t.Fatal("provider did not start")
	}
	go call()
	time.Sleep(50 * time.Millisecond)
	close(client.release)

	for range 2 {
		select {
		case got := <-results:
			if got.err != nil || fmt.Sprint(got.models) != "[fresh-model]" {
				t.Fatalf("unexpected result models=%v err=%v", got.models, got.err)
			}
		case <-time.After(2 * time.Second):
			t.Fatal("concurrent refresh timed out")
		}
	}
	if client.calls.Load() != 1 {
		t.Fatalf("provider calls=%d, want 1", client.calls.Load())
	}
}

func TestRegistryCacheKeyDoesNotExposeAPIKey(t *testing.T) {
	cache := newFakeModelsCache()
	client := &modelClient{models: []string{"model"}}
	registry := NewRegistryWithClients(cache, testFingerprintSecret, map[string]Client{ProviderXAI: client})
	apiKey := "sk-plain-secret-must-never-appear"
	if _, _, err := registry.ListModelsCached(context.Background(), ProviderXAI, apiKey); err != nil {
		t.Fatal(err)
	}

	cache.mu.Lock()
	seen := fmt.Sprintf("%+v %+v", cache.seen, cache.values)
	var fingerprint string
	for key := range cache.values {
		fingerprint = key.CredentialFingerprint
	}
	cache.mu.Unlock()
	if strings.Contains(seen, apiKey) {
		t.Fatalf("cache metadata exposed API key: %s", seen)
	}
	if len(fingerprint) != sha256HexLength || strings.Contains(fingerprint, apiKey) {
		t.Fatalf("invalid credential fingerprint %q", fingerprint)
	}
}

const sha256HexLength = 64
