package replicas

import (
	"math"
	"strings"
	"testing"
	"time"

	"k8s.io/apimachinery/pkg/api/resource"
	"sigs.k8s.io/controller-runtime/pkg/client"

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

type fakeTier struct {
	name      string
	resources []tier.Resource
}

var _ tier.Tier = (*fakeTier)(nil)

func (t *fakeTier) Name() string { return t.name }

func (t *fakeTier) Resources() []tier.Resource { return t.resources }

func (t *fakeTier) Capacity(resourceName string) (resource.Quantity, bool) {
	for i := range t.resources {
		if t.resources[i].Name == resourceName {
			return t.resources[i].Capacity, true
		}
	}
	return resource.Quantity{}, false
}

func (t *fakeTier) Patch() client.Patch { return nil }

func (t *fakeTier) Cost() int64 { return 0 }

type fakeCalculator struct {
	seenMetric *metrics.Metrics
	seenTier   tier.Tier
	result     int32
}

var _ Calculator = (*fakeCalculator)(nil)

func (c *fakeCalculator) Calculate(metric *metrics.Metrics, candidateTier tier.Tier) (int32, error) {
	c.seenMetric = metric
	c.seenTier = candidateTier
	return c.result, nil
}

func TestCalculatorContract(t *testing.T) {
	candidateTier := &fakeTier{
		name: "4c",
		resources: []tier.Resource{
			{Name: "cpu", Capacity: resource.MustParse("4")},
			{Name: "memory", Capacity: resource.MustParse("16Gi")},
		},
	}
	metric := metrics.Metrics{Series: []metrics.TimeSeries{sampleSeries(0.5)}}
	calculator := &fakeCalculator{result: 3}

	got, err := calculator.Calculate(&metric, candidateTier)
	if err != nil {
		t.Fatalf("Calculate() error = %v", err)
	}
	if calculator.seenMetric != &metric {
		t.Fatalf("Calculate() saw metric = %p, want %p", calculator.seenMetric, &metric)
	}
	if calculator.seenTier.Name() != "4c" {
		t.Fatalf("Calculate() saw tier = %q, want %q", calculator.seenTier.Name(), "4c")
	}
	if got != 3 {
		t.Fatalf("Calculate() = %d, want 3", got)
	}
}

func TestNewTargetUtilizationCalculatorRejectsInvalidTarget(t *testing.T) {
	_, err := NewTargetUtilizationCalculator("cpu", 0)
	if err == nil || !strings.Contains(err.Error(), "target average utilization") {
		t.Fatalf("NewTargetUtilizationCalculator() error = %v, want target utilization error", err)
	}
}

func TestNewTargetUtilizationCalculatorRejectsMissingResourceName(t *testing.T) {
	_, err := NewTargetUtilizationCalculator("", 50)
	if err == nil || !strings.Contains(err.Error(), "resource name") {
		t.Fatalf("NewTargetUtilizationCalculator() error = %v, want resource name error", err)
	}
}

func TestTargetUtilizationCalculator(t *testing.T) {
	tests := []struct {
		name          string
		calculator    *TargetUtilizationCalculator
		metric        *metrics.Metrics
		candidateTier tier.Tier
		want          int32
		wantErrText   string
	}{
		{
			name:       "single resource",
			calculator: newCalculator(t, "cpu", 50),
			candidateTier: &fakeTier{name: "4c", resources: []tier.Resource{
				{Name: "cpu", Capacity: resource.MustParse("4")},
			}},
			metric: &metrics.Metrics{Series: []metrics.TimeSeries{sampleSeries(1), sampleSeries(5)}},
			want:   3,
		},
		{
			name:       "direct utilization formula",
			calculator: newCalculator(t, "cpu", 50),
			candidateTier: &fakeTier{name: "1c", resources: []tier.Resource{
				{Name: "cpu", Capacity: resource.MustParse("1")},
			}},
			metric: &metrics.Metrics{Series: []metrics.TimeSeries{sampleSeries(0.334), sampleSeries(0.334), sampleSeries(0.333)}},
			want:   3,
		},
		{
			name:       "range uses latest sample per series",
			calculator: newCalculator(t, "cpu", 50),
			candidateTier: &fakeTier{name: "2c", resources: []tier.Resource{
				{Name: "cpu", Capacity: resource.MustParse("2")},
			}},
			metric: &metrics.Metrics{Series: []metrics.TimeSeries{{Samples: []metrics.Sample{
				{Timestamp: time.Unix(1, 0), Value: 100},
				{Timestamp: time.Unix(2, 0), Value: 1},
			}}}},
			want: 1,
		},
		{
			name:       "zero load recommends zero replicas",
			calculator: newCalculator(t, "cpu", 50),
			candidateTier: &fakeTier{name: "4c", resources: []tier.Resource{
				{Name: "cpu", Capacity: resource.MustParse("4")},
			}},
			metric: &metrics.Metrics{Series: []metrics.TimeSeries{sampleSeries(0), sampleSeries(0)}},
			want:   0,
		},
		{
			name:       "missing tier resource",
			calculator: newCalculator(t, "cpu", 50),
			candidateTier: &fakeTier{name: "4c", resources: []tier.Resource{
				{Name: "memory", Capacity: resource.MustParse("16Gi")},
			}},
			metric:      validMetric(),
			wantErrText: `tier resource "cpu" not found`,
		},
		{
			name:          "empty metric recommends zero replicas",
			calculator:    newCalculator(t, "cpu", 50),
			candidateTier: &fakeTier{name: "4c", resources: []tier.Resource{{Name: "cpu", Capacity: resource.MustParse("4")}}},
			metric:        &metrics.Metrics{},
			want:          0,
		},
		{
			name:          "zero capacity",
			calculator:    newCalculator(t, "cpu", 50),
			candidateTier: &fakeTier{name: "zero", resources: []tier.Resource{{Name: "cpu", Capacity: resource.MustParse("0")}}},
			metric:        validMetric(),
			wantErrText:   "capacity must be greater than zero",
		},
		{
			name:          "non-finite sample",
			calculator:    newCalculator(t, "cpu", 50),
			candidateTier: &fakeTier{name: "4c", resources: []tier.Resource{{Name: "cpu", Capacity: resource.MustParse("4")}}},
			metric:        &metrics.Metrics{Series: []metrics.TimeSeries{sampleSeries(math.NaN())}},
			wantErrText:   "finite non-negative",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := tt.calculator.Calculate(tt.metric, tt.candidateTier)
			if tt.wantErrText != "" {
				if err == nil || !strings.Contains(err.Error(), tt.wantErrText) {
					t.Fatalf("Calculate() error = %v, want containing %q", err, tt.wantErrText)
				}
				return
			}
			if err != nil {
				t.Fatalf("Calculate() error = %v", err)
			}
			if got != tt.want {
				t.Fatalf("Calculate() = %d, want %d", got, tt.want)
			}
		})
	}
}

func newCalculator(t *testing.T, resourceName string, targetAverageUtilization int32) *TargetUtilizationCalculator {
	t.Helper()
	calculator, err := NewTargetUtilizationCalculator(resourceName, targetAverageUtilization)
	if err != nil {
		t.Fatalf("NewTargetUtilizationCalculator() error = %v", err)
	}
	return calculator
}

func validMetric() *metrics.Metrics {
	return &metrics.Metrics{Series: []metrics.TimeSeries{sampleSeries(1)}}
}

func sampleSeries(value float64) metrics.TimeSeries {
	return metrics.TimeSeries{Samples: []metrics.Sample{{Timestamp: time.Unix(1, 0), Value: value}}}
}
