package repository

import (
	"context"
	"errors"
	"strings"
	"testing"
	"time"

	"apps/backend/internal/module/member/domain"
)

type fakeSettingsCache struct {
	value   string
	getErr  error
	deleted bool
}

func (c *fakeSettingsCache) Get(context.Context, string) (string, error) {
	return c.value, c.getErr
}

func (c *fakeSettingsCache) Set(context.Context, string, string, time.Duration) error {
	return nil
}

func (c *fakeSettingsCache) Delete(context.Context, string) error {
	c.deleted = true
	return nil
}

func TestSettingsCacheDTORoundTripRetainsCiphertextWithoutPlaintext(t *testing.T) {
	codec := repositoryTestCodec(t)
	plaintext := &domain.UserSettings{
		UID: 42, Provider: "xai", Model: "grok-3", APIKey: "legacy-plain-secret",
		ProviderAPIKeys: map[string]string{"xai": "provider-plain-secret"}, ExaAPIKey: "exa-plain-secret",
		WebSearchProvider: "exa", ExpandStrategy: "hybrid", UpdatedAt: 123,
	}
	encrypted, err := encryptSettings(codec, plaintext.UID, plaintext)
	if err != nil {
		t.Fatal(err)
	}
	dto, err := newSettingsCacheDTO(encrypted)
	if err != nil {
		t.Fatal(err)
	}
	raw, err := encodeSettingsCache(dto)
	if err != nil {
		t.Fatal(err)
	}
	for _, secret := range []string{plaintext.APIKey, plaintext.ProviderAPIKeys["xai"], plaintext.ExaAPIKey} {
		if strings.Contains(raw, secret) {
			t.Fatalf("serialized cache contains plaintext secret %q: %s", secret, raw)
		}
	}
	decoded, err := decodeSettingsCache(raw, plaintext.UID)
	if err != nil {
		t.Fatal(err)
	}
	got := decoded.settings()
	if got.APIKey != encrypted.APIKey || got.ProviderAPIKeys["xai"] != encrypted.ProviderAPIKeys["xai"] || got.ExaAPIKey != encrypted.ExaAPIKey {
		t.Fatalf("ciphertext did not survive DTO round-trip: %#v", got)
	}
}

func TestSettingsCacheRejectsPlaintextSecrets(t *testing.T) {
	_, err := newSettingsCacheDTO(&domain.UserSettings{UID: 42, APIKey: "plaintext"})
	if err == nil {
		t.Fatal("plaintext cache DTO was accepted")
	}
}

func TestReadSettingsCacheCorruptionDeletesAndFallsBack(t *testing.T) {
	cache := &fakeSettingsCache{value: `{"version":1,"found":true,"uid":42,"api_key":"plaintext"}`}
	got, hit, err := readSettingsCache(context.Background(), cache, "settings-key", 42, repositoryTestCodec(t))
	if err == nil || hit || got != nil {
		t.Fatalf("corrupt lookup = %#v, hit=%v, err=%v", got, hit, err)
	}
	if !cache.deleted {
		t.Fatal("corrupt cache entry was not deleted")
	}
}

func TestReadSettingsCacheOutageFallsBackWithoutDeleting(t *testing.T) {
	cache := &fakeSettingsCache{getErr: errors.New("redis unavailable")}
	got, hit, err := readSettingsCache(context.Background(), cache, "settings-key", 42, repositoryTestCodec(t))
	if err == nil || hit || got != nil {
		t.Fatalf("outage lookup = %#v, hit=%v, err=%v", got, hit, err)
	}
	if cache.deleted {
		t.Fatal("Redis outage should not trigger cache deletion")
	}
}

func TestSettingsCacheKeyUsesConfiguredNamespace(t *testing.T) {
	if got, want := settingsCacheKey("tenant:prod:v2", 42), "tenant:prod:v2:member:settings:{42}"; got != want {
		t.Fatalf("settings cache key=%q, want %q", got, want)
	}
	if got, want := settingsCacheKey("", 42), "haixun:dev:v1:member:settings:{42}"; got != want {
		t.Fatalf("default settings cache key=%q, want %q", got, want)
	}
}
