// This file verifies schedules, point-in-time freshness, skipping, and HPA comparison metrics.
package evaluation

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

	decisionmetrics "github.com/tidbcloud/cluster-service-ng/internal/capacity-planner/pkg/metrics"
	"github.com/tidbcloud/cluster-service-ng/internal/capacity-planner/pkg/tier"
)

func TestReplaySchedules(t *testing.T) {
	start := time.Date(2026, 8, 1, 0, 0, 0, 0, time.UTC)

	interval, err := NewIntervalSchedule(start, start.Add(10*time.Minute), 5*time.Minute)
	if err != nil {
		t.Fatalf("NewIntervalSchedule(): %v", err)
	}
	if got := len(interval.Times); got != 3 {
		t.Fatalf("interval points = %d, want 3", got)
	}
	if !interval.Times[2].Equal(start.Add(10 * time.Minute)) {
		t.Fatalf("last interval point = %s, want %s", interval.Times[2], start.Add(10*time.Minute))
	}

	sparse, err := NewSparseSchedule([]time.Time{start.Add(10 * time.Minute), start, start.Add(10 * time.Minute)})
	if err != nil {
		t.Fatalf("NewSparseSchedule(): %v", err)
	}
	if got := len(sparse.Times); got != 2 {
		t.Fatalf("sparse points = %d, want 2 unique points", got)
	}
	if !sparse.Times[0].Equal(start) || !sparse.Times[1].Equal(start.Add(10*time.Minute)) {
		t.Fatalf("sparse points = %v, want sorted unique points", sparse.Times)
	}
}

func TestRunReplayUsesOnlyFreshPointInTimeSamples(t *testing.T) {
	start := time.Date(2026, 8, 1, 0, 0, 0, 0, time.UTC)
	dataset := testReplayDataset(start)
	schedule, err := NewIntervalSchedule(start, start.Add(10*time.Minute), 5*time.Minute)
	if err != nil {
		t.Fatal(err)
	}

	calls := 0
	candidate := testPolicyFunc(func(metrics []decisionmetrics.Metrics) (tier.Tier, int32, error) {
		calls++
		if len(metrics) != 2 {
			t.Fatalf("policy metrics = %+v, want cpu and memory", metrics)
		}
		for i := range metrics {
			for j := range metrics[i].Series {
				latest := metrics[i].Series[j].Samples[len(metrics[i].Series[j].Samples)-1]
				if latest.Timestamp.After(schedule.Times[calls-1]) {
					t.Fatalf("policy received future sample %s at %s", latest.Timestamp, schedule.Times[calls-1])
				}
			}
		}

		return &testTier{name: "fixed"}, int32(calls + 1), nil
	})

	report, err := RunReplay(context.Background(), dataset, schedule, candidate, ReplayOptions{
		ExperimentID:           "test-experiment",
		ExperimentDigest:       "experiment-digest",
		PolicyRevision:         "test-revision",
		ObservedReplicasMetric: "tidb_up_replicas",
		PolicyMetrics: []PolicyMetricMapping{
			{SourceMetric: "tidb_cpu", PolicyMetric: "cpu"},
			{SourceMetric: "tidb_memory", PolicyMetric: "memory"},
		},
	})
	if err != nil {
		t.Fatalf("RunReplay(): %v", err)
	}
	if calls != 3 {
		t.Fatalf("Choose() calls = %d, want 3", calls)
	}
	if report.Summary.EvaluatedPoints != 3 || report.Summary.SkippedPoints != 0 || report.Summary.PolicyErrorPoints != 0 {
		t.Fatalf("summary = %+v, want three evaluated points", report.Summary)
	}
	if got := report.Decisions[0].Desired.Replicas; got != 2 {
		t.Fatalf("first desired replicas = %d, want 2", got)
	}
	if got := report.Decisions[2].Desired.Replicas; got != 4 {
		t.Fatalf("last desired replicas = %d, want 4", got)
	}
	if report.Summary.RecommendationEpisodes != 3 || report.Summary.TargetChanges != 2 {
		t.Fatalf("temporal summary = %+v, want three episodes and two target changes", report.Summary)
	}
}

func TestRunReplayComparesPolicyWithHPADesiredReplicas(t *testing.T) {
	start := time.Date(2026, 8, 1, 0, 0, 0, 0, time.UTC)
	dataset := testReplayDataset(start)
	dataset.MaxAgeByMetric["hpa_desired_replicas"] = 2 * time.Minute
	dataset.Metrics = append(dataset.Metrics, MetricSeries{
		Name:    "hpa_desired_replicas",
		Samples: replaySamples(start, 2, 3, 2),
	})
	schedule, err := NewIntervalSchedule(start, start.Add(10*time.Minute), 5*time.Minute)
	if err != nil {
		t.Fatal(err)
	}

	policyTargets := []int32{2, 3, 4}
	call := 0
	candidate := testPolicyFunc(func(_ []decisionmetrics.Metrics) (tier.Tier, int32, error) {
		target := policyTargets[call]
		call++

		return &testTier{name: "fixed"}, target, nil
	})
	report, err := RunReplay(context.Background(), dataset, schedule, candidate, ReplayOptions{
		ExperimentID:             "test-experiment",
		ObservedReplicasMetric:   "tidb_up_replicas",
		HPADesiredReplicasMetric: "hpa_desired_replicas",
		PolicyMetrics: []PolicyMetricMapping{
			{SourceMetric: "tidb_cpu", PolicyMetric: "cpu"},
			{SourceMetric: "tidb_memory", PolicyMetric: "memory"},
		},
	})
	if err != nil {
		t.Fatalf("RunReplay(): %v", err)
	}

	if report.Summary.HPAReferencePoints != 3 {
		t.Fatalf("HPA reference points = %d, want 3", report.Summary.HPAReferencePoints)
	}
	if report.Summary.HPATargetChanges != 2 || report.Summary.HPADirectionReversals != 1 {
		t.Fatalf("HPA stability = %+v, want two changes and one reversal", report.Summary)
	}
	if report.Summary.TargetChanges != 2 || report.Summary.DirectionReversals != 0 {
		t.Fatalf("policy stability = %+v, want two changes and no reversal", report.Summary)
	}
	if math.Abs(report.Summary.HPAReplicaHours-5.0/12.0) > 1e-9 {
		t.Fatalf("HPA replica-hours = %.3f, want %.3f", report.Summary.HPAReplicaHours, 5.0/12.0)
	}
	if report.Summary.ScheduledReplicaHourIntervals != 2 || report.Summary.ComparableReplicaHourIntervals != 2 {
		t.Fatalf("replica-hour intervals = %d/%d, want 2/2", report.Summary.ComparableReplicaHourIntervals, report.Summary.ScheduledReplicaHourIntervals)
	}
	if report.Decisions[1].HPADesiredReplicas == nil || *report.Decisions[1].HPADesiredReplicas != 3 {
		t.Fatalf("middle HPA desired replicas = %v, want 3", report.Decisions[1].HPADesiredReplicas)
	}
}

func TestRunReplayCountsReplicaHoursOnlyOnComparableIntervals(t *testing.T) {
	start := time.Date(2026, 8, 1, 0, 0, 0, 0, time.UTC)
	dataset := testReplayDataset(start)
	dataset.MaxAgeByMetric["hpa_desired_replicas"] = 2 * time.Minute
	dataset.Metrics = append(dataset.Metrics, MetricSeries{
		Name: "hpa_desired_replicas",
		Samples: []MetricSample{
			{Timestamp: start, Value: 2},
			{Timestamp: start.Add(10 * time.Minute), Value: 2},
		},
	})
	schedule, err := NewIntervalSchedule(start, start.Add(10*time.Minute), 5*time.Minute)
	if err != nil {
		t.Fatal(err)
	}

	report, err := RunReplay(context.Background(), dataset, schedule, testPolicyFunc(func(_ []decisionmetrics.Metrics) (tier.Tier, int32, error) {
		return &testTier{name: "fixed"}, 3, nil
	}), ReplayOptions{
		ExperimentID:             "test-experiment",
		ObservedReplicasMetric:   "tidb_up_replicas",
		HPADesiredReplicasMetric: "hpa_desired_replicas",
		PolicyMetrics: []PolicyMetricMapping{
			{SourceMetric: "tidb_cpu", PolicyMetric: "cpu"},
			{SourceMetric: "tidb_memory", PolicyMetric: "memory"},
		},
	})
	if err != nil {
		t.Fatalf("RunReplay(): %v", err)
	}

	if report.Summary.ScheduledReplicaHourIntervals != 2 || report.Summary.ComparableReplicaHourIntervals != 1 {
		t.Fatalf("replica-hour intervals = %d/%d, want 1/2", report.Summary.ComparableReplicaHourIntervals, report.Summary.ScheduledReplicaHourIntervals)
	}
	if math.Abs(report.Summary.ObservedReplicaHours-2.0/12.0) > 1e-9 ||
		math.Abs(report.Summary.RecommendedReplicaHours-3.0/12.0) > 1e-9 ||
		math.Abs(report.Summary.HPAReplicaHours-2.0/12.0) > 1e-9 {
		t.Fatalf("replica-hours observed/policy/HPA = %.3f/%.3f/%.3f, want comparable first interval only",
			report.Summary.ObservedReplicaHours, report.Summary.RecommendedReplicaHours, report.Summary.HPAReplicaHours)
	}
}

func TestRunReplaySkipsAmbiguousInstanceCoverageAndRecordsPolicyErrors(t *testing.T) {
	start := time.Date(2026, 8, 1, 0, 0, 0, 0, time.UTC)
	dataset := testReplayDataset(start)
	// Memory for instance b is stale at the middle point, so the policy input
	// cannot represent the two replicas reported by tidb_up_replicas.
	dataset.Metrics[4].Samples = []MetricSample{
		dataset.Metrics[4].Samples[0],
		dataset.Metrics[4].Samples[2],
	}
	schedule, err := NewIntervalSchedule(start, start.Add(10*time.Minute), 5*time.Minute)
	if err != nil {
		t.Fatal(err)
	}

	calls := 0
	candidate := testPolicyFunc(func(_ []decisionmetrics.Metrics) (tier.Tier, int32, error) {
		calls++
		if calls == 2 {
			return nil, 0, errors.New("candidate rejected snapshot")
		}
		return &testTier{name: "fixed"}, 2, nil
	})

	report, err := RunReplay(context.Background(), dataset, schedule, candidate, ReplayOptions{
		ExperimentID:           "test-experiment",
		ObservedReplicasMetric: "tidb_up_replicas",
		PolicyMetrics: []PolicyMetricMapping{
			{SourceMetric: "tidb_cpu", PolicyMetric: "cpu"},
			{SourceMetric: "tidb_memory", PolicyMetric: "memory"},
		},
	})
	if err != nil {
		t.Fatalf("RunReplay(): %v", err)
	}
	if calls != 2 {
		t.Fatalf("Choose() calls = %d, want 2 because one point is skipped", calls)
	}
	if report.Summary.EvaluatedPoints != 1 || report.Summary.SkippedPoints != 1 || report.Summary.PolicyErrorPoints != 1 {
		t.Fatalf("summary = %+v, want one evaluated, one skipped, one policy error", report.Summary)
	}
	if report.Decisions[1].Status != ReplayDecisionSkipped || !strings.Contains(report.Decisions[1].Message, "instances") {
		t.Fatalf("middle decision = %+v, want instance coverage skip", report.Decisions[1])
	}
	if report.Decisions[2].Status != ReplayDecisionPolicyError || !strings.Contains(report.Decisions[2].Message, "candidate rejected") {
		t.Fatalf("last decision = %+v, want policy error", report.Decisions[2])
	}
	if len(report.Episodes) != 1 || report.Episodes[0].StartReason != RecommendationStartInitial {
		t.Fatalf("episodes = %+v, want initial episode before the interrupted points", report.Episodes)
	}
}

func testReplayDataset(start time.Time) *ReplayDataset {
	return &ReplayDataset{
		Version:   ReplayVersionV1Alpha1,
		ID:        "test-dataset",
		Component: "tidb",
		Target:    O11YTargetSpec{ClusterID: "cluster-1", Vendor: "aws", Region: "us-west-2"},
		Window: O11YQueryWindow{
			Start:       start,
			End:         start.Add(10 * time.Minute),
			StepSeconds: 60,
		},
		ArchiveDigest: "archive-digest",
		MaxAgeByMetric: map[string]time.Duration{
			"tidb_up_replicas": 2 * time.Minute,
			"tidb_cpu":         2 * time.Minute,
			"tidb_memory":      2 * time.Minute,
		},
		Metrics: []MetricSeries{
			{Name: "tidb_up_replicas", Samples: replaySamples(start, 2, 2, 2)},
			{Name: "tidb_cpu", Labels: map[string]string{"instance": "a"}, Samples: replaySamples(start, 1, 2, 3)},
			{Name: "tidb_cpu", Labels: map[string]string{"instance": "b"}, Samples: replaySamples(start, 2, 3, 4)},
			{Name: "tidb_memory", Labels: map[string]string{"instance": "a"}, Samples: replaySamples(start, 10, 20, 30)},
			{Name: "tidb_memory", Labels: map[string]string{"instance": "b"}, Samples: replaySamples(start, 20, 30, 40)},
		},
	}
}

func replaySamples(start time.Time, values ...float64) []MetricSample {
	samples := make([]MetricSample, len(values))
	for i := range values {
		samples[i] = MetricSample{Timestamp: start.Add(time.Duration(i) * 5 * time.Minute), Value: values[i]}
	}
	return samples
}
