// This file verifies decision scoring, capacity matching, and suite acceptance metrics.
package evaluation

import (
	"context"
	"fmt"
	"strings"
	"testing"
	"time"

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

type testPolicyFunc func([]decisionmetrics.Metrics) (tier.Tier, int32, error)

var _ productionpolicy.Policy = testPolicyFunc(nil)

func (f testPolicyFunc) Choose(metrics []decisionmetrics.Metrics) (tier.Tier, int32, error) {
	return f(metrics)
}

type fixedCapacityPolicy struct {
	calls           int
	desiredTier     tier.Tier
	desiredReplicas int32
}

var _ productionpolicy.Policy = (*fixedCapacityPolicy)(nil)

func (p *fixedCapacityPolicy) Choose(_ []decisionmetrics.Metrics) (tier.Tier, int32, error) {
	p.calls++
	return p.desiredTier, p.desiredReplicas, nil
}

func TestEvaluateReportsMismatchedCapacity(t *testing.T) {
	bundle := testTiDBCaseBundle()

	result, err := Evaluate(context.Background(), bundle, testPolicyFunc(func(_ []decisionmetrics.Metrics) (tier.Tier, int32, error) {
		return &testTier{name: "S1"}, 4, nil
	}))
	if err != nil {
		t.Fatalf("Evaluate(): %v", err)
	}
	if result.Verdict != VerdictFail {
		t.Fatalf("verdict = %q, want %q", result.Verdict, VerdictFail)
	}
	if len(result.Mismatches) == 0 {
		t.Fatal("mismatches are empty, want capacity mismatch evidence")
	}
	if result.AvoidedReplicas != 0 {
		t.Fatalf("avoided replicas = %d, want 0 for a failed case", result.AvoidedReplicas)
	}
}

func TestEvaluateTiDBNoScaleCase(t *testing.T) {
	bundle := testTiDBCaseBundle()
	policy := &fixedCapacityPolicy{
		desiredTier:     &testTier{name: "S1"},
		desiredReplicas: bundle.Oracle.Expected.Replicas,
	}

	result, err := Evaluate(context.Background(), bundle, policy)
	if err != nil {
		t.Fatalf("Evaluate(): %v", err)
	}
	if policy.calls != 1 {
		t.Fatalf("Choose() calls = %d, want 1", policy.calls)
	}
	if result.Verdict != VerdictPass {
		t.Fatalf("verdict = %q, want %q; mismatches = %v", result.Verdict, VerdictPass, result.Mismatches)
	}
	if result.HorizontalChange != HorizontalNoScale {
		t.Fatalf("horizontal change = %q, want %q", result.HorizontalChange, HorizontalNoScale)
	}
	if result.AvoidedReplicas != 1 {
		t.Fatalf("avoided replicas = %d, want 1", result.AvoidedReplicas)
	}
}

func TestEvaluateRejectsFutureMetricSamplesBeforeCallingPolicy(t *testing.T) {
	bundle := testTiDBCaseBundle()
	bundle.Input.Metrics[0].Samples = append(bundle.Input.Metrics[0].Samples, MetricSample{
		Timestamp: bundle.Input.DecisionAt.Add(time.Second),
		Value:     0.99,
	})
	policy := &fixedCapacityPolicy{
		desiredTier:     &testTier{name: "S1"},
		desiredReplicas: bundle.Oracle.Expected.Replicas,
	}

	_, err := Evaluate(context.Background(), bundle, policy)
	if err == nil || !strings.Contains(err.Error(), "after decisionAt") {
		t.Fatalf("Evaluate() error = %v, want future sample error", err)
	}
	if policy.calls != 0 {
		t.Fatalf("Choose() calls = %d, want 0", policy.calls)
	}
}

func TestEvaluateRejectsPolicyCapacityWithoutTier(t *testing.T) {
	bundle := testTiDBCaseBundle()

	_, err := Evaluate(context.Background(), bundle, testPolicyFunc(func(_ []decisionmetrics.Metrics) (tier.Tier, int32, error) {
		return nil, 3, nil
	}))
	if err == nil || !strings.Contains(err.Error(), "actual.tier is required") {
		t.Fatalf("Evaluate() error = %v, want missing tier error", err)
	}
}

func TestEvaluateSuiteReportsAcceptanceMetrics(t *testing.T) {
	start := time.Date(2026, 6, 13, 13, 0, 0, 0, time.UTC)
	bundles := []*CaseBundle{
		testTiDBCaseAt("useful-accepted", start, OracleClassUsefulScaleOut, Capacity{Replicas: 4}),
		testTiDBCaseAt("useful-missed", start.Add(10*time.Minute), OracleClassUsefulScaleOut, Capacity{Replicas: 4}),
		testTiDBCaseAt("unnecessary-rejected", start.Add(20*time.Minute), OracleClassUnnecessaryScaleOut, Capacity{Replicas: 3}),
		testTiDBCaseAt("unnecessary-accepted", start.Add(30*time.Minute), OracleClassUnnecessaryScaleOut, Capacity{Replicas: 3}),
	}
	desired := map[time.Time]Capacity{
		bundles[0].Input.DecisionAt: {Tier: "S1", Replicas: 4},
		bundles[1].Input.DecisionAt: {Tier: "S1", Replicas: 3},
		bundles[2].Input.DecisionAt: {Tier: "S1", Replicas: 3},
		bundles[3].Input.DecisionAt: {Tier: "S1", Replicas: 4},
	}

	suite, err := EvaluateSuite(context.Background(), bundles, testPolicyFunc(func(metrics []decisionmetrics.Metrics) (tier.Tier, int32, error) {
		decisionAt := metrics[0].Series[0].Samples[0].Timestamp
		capacity := desired[decisionAt]
		return &testTier{name: capacity.Tier}, capacity.Replicas, nil
	}))
	if err != nil {
		t.Fatalf("EvaluateSuite(): %v", err)
	}

	metrics := suite.Metrics
	if metrics.TotalCases != 4 || metrics.PassedCases != 2 || metrics.FailedCases != 2 {
		t.Errorf("case counts = total %d passed %d failed %d, want 4/2/2", metrics.TotalCases, metrics.PassedCases, metrics.FailedCases)
	}
	if metrics.UsefulScaleOutCases != 2 || metrics.UsefulScaleOutAccepted != 1 || metrics.UsefulScaleOutRecall != 0.5 {
		t.Errorf("useful ScaleOut metrics = %+v, want 2 cases, 1 accepted, 0.5 recall", metrics)
	}
	if metrics.UnnecessaryScaleOutCases != 2 || metrics.UnnecessaryScaleOutRejected != 1 || metrics.UnnecessaryScaleOutRejectionRate != 0.5 {
		t.Errorf("unnecessary ScaleOut metrics = %+v, want 2 cases, 1 rejected, 0.5 rejection rate", metrics)
	}
	if metrics.AvoidedReplicas != 1 {
		t.Errorf("avoided replicas = %d, want 1", metrics.AvoidedReplicas)
	}
}

// Trivial policies must be reported alongside any policy result. On the
// catalog's 81/31 split each one maxes out a headline metric without reading a
// single sample, so neither rejection rate nor recall means anything on its own.
func TestTrivialPoliciesBoundSuiteMetrics(t *testing.T) {
	bundles := testCatalogShapedBundles(81, 31)

	tests := []struct {
		name              string
		policy            productionpolicy.Policy
		wantPassed        int
		wantRecall        float64
		wantRejectionRate float64
		wantAvoided       int32
	}{
		{
			name:              "AlwaysNoScale maxes rejection and avoided replicas",
			policy:            AlwaysNoScale(&testTier{name: "S1"}),
			wantPassed:        81,
			wantRecall:        0,
			wantRejectionRate: 1,
			wantAvoided:       81,
		},
		{
			name:              "AlwaysScaleOut maxes recall",
			policy:            AlwaysScaleOut(&testTier{name: "S1"}),
			wantPassed:        31,
			wantRecall:        1,
			wantRejectionRate: 0,
			wantAvoided:       0,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			suite, err := EvaluateSuite(context.Background(), bundles, tt.policy)
			if err != nil {
				t.Fatalf("EvaluateSuite(): %v", err)
			}
			metrics := suite.Metrics
			if metrics.TotalCases != 112 || metrics.PassedCases != tt.wantPassed {
				t.Errorf("cases = total %d passed %d, want 112/%d", metrics.TotalCases, metrics.PassedCases, tt.wantPassed)
			}
			if metrics.UsefulScaleOutRecall != tt.wantRecall {
				t.Errorf("recall = %v, want %v", metrics.UsefulScaleOutRecall, tt.wantRecall)
			}
			if metrics.UnnecessaryScaleOutRejectionRate != tt.wantRejectionRate {
				t.Errorf("rejection rate = %v, want %v", metrics.UnnecessaryScaleOutRejectionRate, tt.wantRejectionRate)
			}
			if metrics.AvoidedReplicas != tt.wantAvoided {
				t.Errorf("avoided replicas = %d, want %d", metrics.AvoidedReplicas, tt.wantAvoided)
			}
		})
	}
}

// TestAvoidedReplicasAgreeWithSuiteTotal guards the per-case and aggregate
// reporting against divergence.
func TestAvoidedReplicasAgreeWithSuiteTotal(t *testing.T) {
	suite, err := EvaluateSuite(context.Background(), testCatalogShapedBundles(3, 2), AlwaysNoScale(&testTier{name: "S1"}))
	if err != nil {
		t.Fatalf("EvaluateSuite(): %v", err)
	}

	var sum int32
	for i := range suite.Results {
		sum += suite.Results[i].AvoidedReplicas
	}
	if sum != suite.Metrics.AvoidedReplicas {
		t.Errorf("per-case avoided replicas sum = %d, suite total = %d", sum, suite.Metrics.AvoidedReplicas)
	}
}

func testCatalogShapedBundles(unnecessary, useful int) []*CaseBundle {
	start := time.Date(2026, 6, 13, 13, 0, 0, 0, time.UTC)
	bundles := make([]*CaseBundle, 0, unnecessary+useful)
	for i := range unnecessary {
		bundles = append(bundles, testTiDBCaseAt(
			fmt.Sprintf("unnecessary-%d", i),
			start.Add(time.Duration(i)*time.Hour),
			OracleClassUnnecessaryScaleOut,
			Capacity{Replicas: 3},
		))
	}
	for i := range useful {
		bundles = append(bundles, testTiDBCaseAt(
			fmt.Sprintf("useful-%d", i),
			start.Add(time.Duration(unnecessary+i)*time.Hour),
			OracleClassUsefulScaleOut,
			Capacity{Replicas: 4},
		))
	}
	return bundles
}

func testTiDBCaseBundle() *CaseBundle {
	decisionAt := time.Date(2026, 6, 13, 13, 34, 30, 0, time.UTC)
	return testTiDBCaseAt("tidb-no-scale", decisionAt, OracleClassUnnecessaryScaleOut, Capacity{Replicas: 3})
}

func testTiDBCaseAt(id string, decisionAt time.Time, class OracleClass, expected Capacity) *CaseBundle {
	return &CaseBundle{
		Version: CaseBundleVersionV1Alpha1,
		ID:      id,
		Source:  DataSourceManual,
		Input: CaseInput{
			DecisionAt: decisionAt,
			Component:  "tidb",
			Current:    Capacity{Replicas: 3},
			Metrics: []MetricSeries{
				{
					Name:   "tidb_server_connections",
					Labels: map[string]string{"instance": "tidb-0"},
					Samples: []MetricSample{
						{Timestamp: decisionAt.Add(-time.Minute), Value: 10},
						{Timestamp: decisionAt, Value: 10},
					},
				},
				{
					Name:   "tidb_server_connections",
					Labels: map[string]string{"instance": "tidb-1"},
					Samples: []MetricSample{
						{Timestamp: decisionAt.Add(-time.Minute), Value: 10},
						{Timestamp: decisionAt, Value: 10},
					},
				},
				{
					Name:   "tidb_server_connections",
					Labels: map[string]string{"instance": "tidb-2"},
					Samples: []MetricSample{
						{Timestamp: decisionAt.Add(-time.Minute), Value: 10},
						{Timestamp: decisionAt, Value: 10},
					},
				},
			},
		},
		Baseline: Capacity{Replicas: 4},
		Oracle: Oracle{
			Class:    class,
			Expected: expected,
		},
	}
}
