package payments

import (
	"context"
	"testing"
	"time"

	"github.com/google/uuid"
)

// base is a fixed clock instant for the deterministic tests.
var base = time.Date(2026, 7, 8, 12, 0, 0, 0, time.UTC)

// seededService builds a Service whose read cache already holds st for id, so the read methods
// resolve without a database (the store's db is nil). The clock is pinned to base.
func seededService(id uuid.UUID, st walletState) *Service {
	store := NewStore(nil)
	store.cache.put(id, st)
	return &Service{store: store, clock: func() time.Time { return base }}
}

func TestStackNoAds(t *testing.T) {
	future := base.Add(48 * time.Hour)
	past := base.Add(-time.Hour)
	tests := []struct {
		name    string
		current *time.Time
		addDays int
		want    *time.Time
	}{
		{"fresh term", nil, 3, new(base.Add(72 * time.Hour))},
		{"stack onto future", new(future), 2, new(future.Add(48 * time.Hour))},
		{"lapsed term restarts from now", new(past), 1, new(base.Add(24 * time.Hour))},
		{"zero days unchanged", new(future), 0, new(future)},
		{"zero days, nil stays nil", nil, 0, nil},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			got := stackNoAds(tc.current, tc.addDays, base)
			if (got == nil) != (tc.want == nil) || (got != nil && !got.Equal(*tc.want)) {
				t.Errorf("stackNoAds = %v, want %v", got, tc.want)
			}
		})
	}
}

func TestCombineNoAds(t *testing.T) {
	pFuture := base.Add(24 * time.Hour)
	sFuture := base.Add(72 * time.Hour) // 3 days remaining
	// primary future + secondary future: primary end + secondary's remaining (72h).
	if got := combineNoAds(new(pFuture), new(sFuture), base); !got.Equal(pFuture.Add(72 * time.Hour)) {
		t.Errorf("combine both future = %v, want %v", got, pFuture.Add(72*time.Hour))
	}
	// primary nil + secondary future: now + secondary's remaining.
	if got := combineNoAds(nil, new(sFuture), base); !got.Equal(base.Add(72 * time.Hour)) {
		t.Errorf("combine nil primary = %v, want %v", got, base.Add(72*time.Hour))
	}
	// secondary lapsed: primary unchanged.
	if got := combineNoAds(new(pFuture), new(base.Add(-time.Hour)), base); !got.Equal(pFuture) {
		t.Errorf("combine lapsed secondary = %v, want %v", got, pFuture)
	}
	// secondary nil: primary unchanged.
	if got := combineNoAds(new(pFuture), nil, base); !got.Equal(pFuture) {
		t.Errorf("combine nil secondary = %v, want %v", got, pFuture)
	}
}

func TestPlanDraws(t *testing.T) {
	st := walletState{chips: map[Source]int{SourceDirect: 30, SourceVK: 50, SourceTelegram: 5}}
	// price met entirely by the first (direct) segment.
	if draws, ok := planDraws(st, []Source{SourceDirect, SourceVK}, 20); !ok || len(draws) != 1 || draws[0] != (sourceAmount{SourceDirect, 20}) {
		t.Errorf("single-segment draw = %v ok=%v", draws, ok)
	}
	// price spills from direct into vk by priority.
	draws, ok := planDraws(st, []Source{SourceDirect, SourceVK, SourceTelegram}, 60)
	want := []sourceAmount{{SourceDirect, 30}, {SourceVK, 30}}
	if !ok || len(draws) != 2 || draws[0] != want[0] || draws[1] != want[1] {
		t.Errorf("priority spill draw = %v ok=%v, want %v", draws, ok, want)
	}
	// insufficient total.
	if _, ok := planDraws(st, []Source{SourceDirect, SourceVK, SourceTelegram}, 200); ok {
		t.Error("expected insufficient")
	}
}

func TestWalletSegments(t *testing.T) {
	id := uuid.New()
	st := walletState{chips: map[Source]int{SourceDirect: 100, SourceVK: 50}}
	svc := seededService(id, st)
	present := []Source{SourceDirect, SourceVK}

	// Web/native: both attached segments shown, both spendable.
	got, err := svc.Wallet(context.Background(), id, NewContext("direct", "web"), present)
	if err != nil {
		t.Fatal(err)
	}
	if len(got.Segments) != 2 || !got.Segments[0].Spendable || got.Segments[0].Source != SourceDirect {
		t.Errorf("direct wallet segments = %+v", got.Segments)
	}
	// VK Android: only vk shown, spendable.
	got, _ = svc.Wallet(context.Background(), id, NewContext("vk", "android"), present)
	if len(got.Segments) != 1 || got.Segments[0].Source != SourceVK || !got.Segments[0].Spendable {
		t.Errorf("vk-android wallet = %+v", got.Segments)
	}
	// VK iOS: only vk shown, and spendable — the freeze is purchase-only, so VK-wallet chips still
	// spend there (only buying more chips for money is blocked).
	got, _ = svc.Wallet(context.Background(), id, NewContext("vk", "ios"), present)
	if len(got.Segments) != 1 || got.Segments[0].Chips != 50 || !got.Segments[0].Spendable {
		t.Errorf("vk-ios wallet = %+v (want vk 50 spendable)", got.Segments)
	}
}

func TestAdFreeByContext(t *testing.T) {
	id := uuid.New()
	future := base.Add(48 * time.Hour)
	ctx := context.Background()

	// A vk-origin no-ads term applies inside VK and out on web, but a direct-origin term never
	// applies inside VK (the compliance wall).
	svc := seededService(id, walletState{benefits: map[Source]benefitState{
		SourceVK:     {adsPaidUntil: new(future)},
		SourceDirect: {adsPaidUntil: new(future)},
	}})
	present := []Source{SourceDirect, SourceVK}

	cases := []struct {
		name string
		cxt  Context
		want bool
	}{
		{"vk term inside vk", NewContext("vk", "android"), true},
		{"vk term inside vk-ios (applies while frozen)", NewContext("vk", "ios"), true},
		{"terms on web", NewContext("direct", "web"), true},
		{"untrusted fail-closed", NewContext("", ""), false},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			got, err := svc.AdFree(ctx, id, tc.cxt, present)
			if err != nil || got != tc.want {
				t.Errorf("AdFree = %v (err %v), want %v", got, err, tc.want)
			}
		})
	}

	// Compliance: ONLY a direct-origin term, checked inside VK, must not suppress ads.
	svc2 := seededService(id, walletState{benefits: map[Source]benefitState{SourceDirect: {adsPaidUntil: new(future)}}})
	if got, _ := svc2.AdFree(ctx, id, NewContext("vk", "android"), present); got {
		t.Error("direct-origin no-ads must NOT apply inside VK (compliance wall)")
	}
}

func TestHintsAvailableByContext(t *testing.T) {
	id := uuid.New()
	svc := seededService(id, walletState{benefits: map[Source]benefitState{
		SourceDirect: {hints: 3},
		SourceVK:     {hints: 2},
	}})
	present := []Source{SourceDirect, SourceVK}
	ctx := context.Background()

	// Web: both applicable origins summed.
	if n, _ := svc.HintsAvailable(ctx, id, NewContext("direct", "web"), present); n != 5 {
		t.Errorf("web hints = %d, want 5", n)
	}
	// VK: only vk-origin hints.
	if n, _ := svc.HintsAvailable(ctx, id, NewContext("vk", "android"), present); n != 2 {
		t.Errorf("vk hints = %d, want 2", n)
	}
	// Untrusted: none.
	if n, _ := svc.HintsAvailable(ctx, id, NewContext("", ""), present); n != 0 {
		t.Errorf("untrusted hints = %d, want 0", n)
	}
}

func TestSpendHintUntrustedNoOp(t *testing.T) {
	id := uuid.New()
	svc := seededService(id, walletState{benefits: map[Source]benefitState{SourceDirect: {hints: 3}}})
	// Untrusted context: no applicable origin, so nothing is spent and the DB (nil) is never hit.
	spent, err := svc.SpendHint(context.Background(), id, NewContext("", ""), []Source{SourceDirect})
	if err != nil || spent {
		t.Errorf("untrusted SpendHint = %v (err %v), want false", spent, err)
	}
}
