// This file verifies committed catalog loading and lower-bound policy results.
package evaluation

import (
	"context"
	"os"
	"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 countingPolicy struct {
	delegate productionpolicy.Policy
	calls    int
}

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

func (p *countingPolicy) Choose(metrics []decisionmetrics.Metrics) (tier.Tier, int32, error) {
	p.calls++

	return p.delegate.Choose(metrics)
}

func TestLoadCatalogReplaysCommittedTiDBArchives(t *testing.T) {
	const maxMetricAge = 2 * time.Minute

	catalog, err := LoadCatalog("cases")
	if err != nil {
		t.Fatalf("LoadCatalog(): %v", err)
	}
	if len(catalog.Cases) != 112 {
		t.Fatalf("catalog cases = %d, want 112", len(catalog.Cases))
	}

	classes := make(map[OracleClass]int)
	for i := range catalog.Cases {
		bundle := catalog.Cases[i]
		if bundle.Source != DataSourceO11Y || bundle.Input.Component != "tidb" {
			t.Errorf("case %q source/component = %q/%q, want o11y/tidb", bundle.ID, bundle.Source, bundle.Input.Component)
		}
		if len(bundle.Input.Metrics) == 0 {
			t.Errorf("case %q has no replayed metrics", bundle.ID)
		}
		for j := range bundle.Input.Metrics {
			series := &bundle.Input.Metrics[j]
			if series.Labels[metricInstanceLabel] == "" {
				continue
			}
			latest := series.Samples[len(series.Samples)-1].Timestamp
			if bundle.Input.DecisionAt.Sub(latest) > maxMetricAge {
				t.Errorf("case %q metric %q includes a stopped instance last seen at %s", bundle.ID, series.Name, latest)
			}
		}
		if i > 0 && catalog.Cases[i-1].ID >= bundle.ID {
			t.Errorf("catalog order = %q before %q, want increasing case IDs", catalog.Cases[i-1].ID, bundle.ID)
		}
		classes[bundle.Oracle.Class]++
	}

	if got := classes[OracleClassUnnecessaryScaleOut]; got != 81 {
		t.Errorf("UnnecessaryScaleOut cases = %d, want 81", got)
	}
	if got := classes[OracleClassUsefulScaleOut]; got != 31 {
		t.Errorf("UsefulScaleOut cases = %d, want 31", got)
	}
}

func TestEvaluateCatalogReportsRealArchiveBaselines(t *testing.T) {
	catalog, err := LoadCatalog("cases")
	if err != nil {
		t.Fatalf("LoadCatalog(): %v", err)
	}

	tests := []struct {
		name              string
		delegate          productionpolicy.Policy
		wantPassed        int
		wantAccepted      int
		wantRejected      int
		wantRecall        float64
		wantRejectionRate float64
		wantAvoided       int32
	}{
		{
			name:              "AlwaysNoScale",
			delegate:          AlwaysNoScale(&testTier{name: "S1"}),
			wantPassed:        82,
			wantAccepted:      1,
			wantRejected:      81,
			wantRecall:        1.0 / 31.0,
			wantRejectionRate: 1,
			wantAvoided:       86,
		},
		{
			name:              "AlwaysScaleOut",
			delegate:          AlwaysScaleOut(&testTier{name: "S1"}),
			wantPassed:        29,
			wantAccepted:      31,
			wantRejected:      0,
			wantRecall:        1,
			wantRejectionRate: 0,
			wantAvoided:       0,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			candidate := &countingPolicy{delegate: tt.delegate}
			suite, evalErr := EvaluateCatalog(context.Background(), catalog, candidate)
			if evalErr != nil {
				t.Fatalf("EvaluateCatalog(): %v", evalErr)
			}
			if candidate.calls != 112 {
				t.Errorf("Choose() calls = %d, want 112", candidate.calls)
			}

			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.UsefulScaleOutCases != 31 || metrics.UsefulScaleOutAccepted != tt.wantAccepted || metrics.UsefulScaleOutRecall != tt.wantRecall {
				t.Errorf("useful ScaleOut = %d accepted %d recall %v; want 31/%d/%v", metrics.UsefulScaleOutCases, metrics.UsefulScaleOutAccepted, metrics.UsefulScaleOutRecall, tt.wantAccepted, tt.wantRecall)
			}
			if metrics.UnnecessaryScaleOutCases != 81 || metrics.UnnecessaryScaleOutRejected != tt.wantRejected || metrics.UnnecessaryScaleOutRejectionRate != tt.wantRejectionRate {
				t.Errorf("unnecessary ScaleOut = %d rejected %d rejection rate %v; want 81/%d/%v", metrics.UnnecessaryScaleOutCases, metrics.UnnecessaryScaleOutRejected, metrics.UnnecessaryScaleOutRejectionRate, tt.wantRejected, tt.wantRejectionRate)
			}
			if metrics.AvoidedReplicas != tt.wantAvoided {
				t.Errorf("avoided replicas = %d, want %d", metrics.AvoidedReplicas, tt.wantAvoided)
			}
		})
	}
}

func TestReplayO11YArchiveUsesRawResponses(t *testing.T) {
	const casePath = "cases/tidb/point-in-time/tidb-scale-out-20260613T133430Z-32fc79a71fdd.yaml"

	spec, err := loadCaseSpec(casePath)
	if err != nil {
		t.Fatalf("loadCaseSpec(): %v", err)
	}
	profile, err := LoadMetricsProfile("cases", spec.Metrics.Profile)
	if err != nil {
		t.Fatalf("LoadMetricsProfile(): %v", err)
	}
	exportSpec, err := BuildO11YExportSpec(spec, profile)
	if err != nil {
		t.Fatalf("BuildO11YExportSpec(): %v", err)
	}

	archiveFile, err := os.Open(strings.TrimSuffix(casePath, ".yaml") + ".json")
	if err != nil {
		t.Fatalf("open archive: %v", err)
	}
	archive, decodeErr := DecodeO11YArchive(archiveFile)
	closeErr := archiveFile.Close()
	if decodeErr != nil || closeErr != nil {
		t.Fatalf("decode/close archive: %v / %v", decodeErr, closeErr)
	}

	// Derived archive fields may come from an older normalizer. Raw responses
	// and fixed request identity are the replay source of truth.
	archive.Quality.IsPartial = true
	archive.Errors = []string{"stale derived error"}
	archive.Queries[0].SeriesCount = 0
	archive.Queries[0].SampleCount = 0
	archive.Queries[0].Error = "stale derived error"

	bundle, err := ReplayO11YArchive(exportSpec, archive)
	if err != nil {
		t.Fatalf("ReplayO11YArchive(): %v", err)
	}
	if len(bundle.Input.Metrics) == 0 || bundle.Quality.IsPartial {
		t.Fatalf("replayed bundle = %+v, want metrics recomputed from raw responses", bundle)
	}

	archive.Queries[0].RequestDigest = "stale"
	_, err = ReplayO11YArchive(exportSpec, archive)
	if err == nil || !strings.Contains(err.Error(), "request digest") {
		t.Fatalf("ReplayO11YArchive() error = %v, want request identity mismatch", err)
	}
}
