package ads

import (
	"reflect"
	"testing"
	"time"
)

func TestComputeActiveSet(t *testing.T) {
	now := time.Date(2026, 6, 15, 12, 0, 0, 0, time.UTC)
	past := now.Add(-24 * time.Hour)
	future := now.Add(24 * time.Hour)

	// msg builds a one-message slice with distinct bodies so resolution and
	// campaign identity are visible in assertions.
	msg := func(tag string) []Message {
		return []Message{{BodyEn: tag + "-en", BodyRu: tag + "-ru"}}
	}
	def := func(msgs []Message) Campaign {
		return Campaign{Name: "default", Weight: 100, IsDefault: true, Enabled: true, Messages: msgs}
	}
	timed := func(name string, weight int, starts, ends *time.Time, msgs []Message) Campaign {
		return Campaign{Name: name, Weight: weight, Enabled: true, StartsAt: starts, EndsAt: ends, Messages: msgs}
	}
	urgent := func(name string, weight int, msgs []Message) Campaign {
		c := timed(name, weight, nil, nil, msgs)
		c.Urgent = true
		return c
	}
	red := &ColorSet{Bg: "#aa0000", Fg: "#ffffff", Link: "#ffdd00"}

	tests := []struct {
		name       string
		campaigns  []Campaign
		lang       string
		want       []ActiveCampaign
		wantUrgent bool
	}{
		{
			name:      "default only reduces to weight 1",
			campaigns: []Campaign{def(msg("house"))},
			lang:      "en",
			want:      []ActiveCampaign{{Weight: 1, Messages: []string{"house-en"}}},
		},
		{
			name:      "default fills the remainder",
			campaigns: []Campaign{def(msg("house")), timed("promo", 30, nil, nil, msg("promo"))},
			lang:      "en",
			// timed 30, default 100-30=70; gcd 10 -> 3 and 7; timed first, default last.
			want: []ActiveCampaign{
				{Weight: 3, Messages: []string{"promo-en"}},
				{Weight: 7, Messages: []string{"house-en"}},
			},
		},
		{
			name:      "timed fills 100, default dropped",
			campaigns: []Campaign{def(msg("house")), timed("promo", 100, nil, nil, msg("promo"))},
			lang:      "en",
			want:      []ActiveCampaign{{Weight: 1, Messages: []string{"promo-en"}}},
		},
		{
			name: "timed over 100, default dropped, proportional",
			campaigns: []Campaign{
				def(msg("house")),
				timed("a", 60, nil, nil, msg("a")),
				timed("b", 80, nil, nil, msg("b")),
			},
			lang: "en",
			// default dropped (sum 140 >= 100); gcd(60,80)=20 -> 3 and 4.
			want: []ActiveCampaign{
				{Weight: 3, Messages: []string{"a-en"}},
				{Weight: 4, Messages: []string{"b-en"}},
			},
		},
		{
			name: "future and past windows exclude timed",
			campaigns: []Campaign{
				def(msg("house")),
				timed("future", 50, &future, nil, msg("future")),
				timed("past", 50, nil, &past, msg("past")),
			},
			lang: "en",
			want: []ActiveCampaign{{Weight: 1, Messages: []string{"house-en"}}},
		},
		{
			name: "active window included",
			campaigns: []Campaign{
				def(msg("house")),
				timed("live", 25, &past, &future, msg("live")),
			},
			lang: "en",
			// timed 25, default 75; gcd 25 -> 1 and 3.
			want: []ActiveCampaign{
				{Weight: 1, Messages: []string{"live-en"}},
				{Weight: 3, Messages: []string{"house-en"}},
			},
		},
		{
			name: "campaign without messages is skipped and not counted",
			campaigns: []Campaign{
				def(msg("house")),
				timed("empty", 40, nil, nil, nil),
			},
			lang: "en",
			// empty campaign skipped; default fills full 100 -> reduced to 1.
			want: []ActiveCampaign{{Weight: 1, Messages: []string{"house-en"}}},
		},
		{
			name:      "russian bodies resolved",
			campaigns: []Campaign{def(msg("house"))},
			lang:      "ru",
			want:      []ActiveCampaign{{Weight: 1, Messages: []string{"house-ru"}}},
		},
		{
			name: "three timed split by gcd",
			campaigns: []Campaign{
				def(msg("house")),
				timed("a", 50, nil, nil, msg("a")),
				timed("b", 30, nil, nil, msg("b")),
				timed("c", 20, nil, nil, msg("c")),
			},
			lang: "en",
			// sum 100, default dropped; gcd 10 -> 5,3,2.
			want: []ActiveCampaign{
				{Weight: 5, Messages: []string{"a-en"}},
				{Weight: 3, Messages: []string{"b-en"}},
				{Weight: 2, Messages: []string{"c-en"}},
			},
		},
		{
			name: "campaign with multiple messages keeps order",
			campaigns: []Campaign{
				def([]Message{{BodyEn: "one-en", BodyRu: "one-ru"}, {BodyEn: "two-en", BodyRu: "two-ru"}}),
			},
			lang: "en",
			want: []ActiveCampaign{{Weight: 1, Messages: []string{"one-en", "two-en"}}},
		},
		{
			name: "urgent preempts default and normal timed",
			campaigns: []Campaign{
				def(msg("house")),
				timed("promo", 40, nil, nil, msg("promo")),
				urgent("alert", 50, msg("alert")),
			},
			lang: "en",
			// only the urgent campaign survives; a lone weight reduces to 1.
			want:       []ActiveCampaign{{Weight: 1, Messages: []string{"alert-en"}}},
			wantUrgent: true,
		},
		{
			name: "multiple urgent share the feed by gcd",
			campaigns: []Campaign{
				def(msg("house")),
				urgent("a", 60, msg("a")),
				urgent("b", 40, msg("b")),
			},
			lang: "en",
			// default and any non-urgent dropped; gcd(60,40)=20 -> 3 and 2.
			want: []ActiveCampaign{
				{Weight: 3, Messages: []string{"a-en"}},
				{Weight: 2, Messages: []string{"b-en"}},
			},
			wantUrgent: true,
		},
		{
			name: "out-of-window urgent does not preempt",
			campaigns: []Campaign{
				def(msg("house")),
				func() Campaign { c := urgent("future", 50, msg("future")); c.StartsAt = &future; return c }(),
			},
			lang: "en",
			// the urgent campaign is not yet live, so the normal default feed stands.
			want: []ActiveCampaign{{Weight: 1, Messages: []string{"house-en"}}},
		},
		{
			name: "colour overrides ride the active campaign",
			campaigns: []Campaign{
				def(msg("house")),
				func() Campaign { c := timed("promo", 100, nil, nil, msg("promo")); c.OverrideAll = red; return c }(),
			},
			lang: "en",
			want: []ActiveCampaign{{Weight: 1, Messages: []string{"promo-en"}, OverrideAll: red}},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, gotUrgent := computeActiveSet(tt.campaigns, now, tt.lang)
			if !reflect.DeepEqual(got, tt.want) {
				t.Errorf("computeActiveSet() =\n  %#v\nwant\n  %#v", got, tt.want)
			}
			if gotUrgent != tt.wantUrgent {
				t.Errorf("computeActiveSet() urgent = %v, want %v", gotUrgent, tt.wantUrgent)
			}
		})
	}
}

func TestComputeActiveSetEmpty(t *testing.T) {
	// No campaigns at all yields an empty (non-nil-or-nil) feed without panicking.
	if got, urgent := computeActiveSet(nil, time.Now(), "en"); len(got) != 0 || urgent {
		t.Errorf("computeActiveSet(nil) = %#v urgent=%v, want empty non-urgent", got, urgent)
	}
}

func TestGCD(t *testing.T) {
	tests := []struct{ a, b, want int }{
		{0, 5, 5}, {5, 0, 5}, {12, 18, 6}, {100, 100, 100}, {7, 13, 1},
	}
	for _, tt := range tests {
		if got := gcd(tt.a, tt.b); got != tt.want {
			t.Errorf("gcd(%d,%d) = %d, want %d", tt.a, tt.b, got, tt.want)
		}
	}
}
