package policy

import (
	"errors"
	"math"
	"strings"
	"testing"

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

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

type fakeTier struct {
	name string
	cost int64
}

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

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

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

func (t *fakeTier) Capacity(string) (resource.Quantity, bool) {
	return resource.Quantity{}, false
}

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

func (t *fakeTier) Cost() int64 {
	return t.cost
}

type fakeCalculator struct {
	byTier map[string]int32
	err    error
}

var _ replicas.Calculator = (*fakeCalculator)(nil)

func (c *fakeCalculator) Calculate(_ *metrics.Metrics, candidateTier tier.Tier) (int32, error) {
	if c.err != nil {
		return 0, c.err
	}
	return c.byTier[candidateTier.Name()], nil
}

func TestCostOptimizedChoosesLowestTotalCostAcrossAllMetrics(t *testing.T) {
	small := &fakeTier{name: "small", cost: 3}
	large := &fakeTier{name: "large", cost: 4}
	policy, err := NewCostOptimized([]tier.Tier{small, large}, []replicas.Calculator{
		&fakeCalculator{byTier: map[string]int32{"small": 4, "large": 2}},
		&fakeCalculator{byTier: map[string]int32{"small": 3, "large": 1}},
	}, nil)
	if err != nil {
		t.Fatalf("NewCostOptimized(): %v", err)
	}

	gotTier, gotReplicas, err := policy.Choose([]metrics.Metrics{{}, {}})
	if err != nil {
		t.Fatalf("Choose(): %v", err)
	}
	if gotTier.Name() != "large" {
		t.Fatalf("tier = %q, want large", gotTier.Name())
	}
	if gotReplicas != 2 {
		t.Fatalf("replicas = %d, want 2", gotReplicas)
	}
}

func TestCostOptimizedAppliesReplicaBounds(t *testing.T) {
	small := &fakeTier{name: "small", cost: 1}
	large := &fakeTier{name: "large", cost: 4}
	policy, err := NewCostOptimized([]tier.Tier{small, large}, []replicas.Calculator{
		&fakeCalculator{byTier: map[string]int32{"small": 5, "large": 2}},
	}, &v1alpha1.ReplicasSpec{Min: 3, Max: 3})
	if err != nil {
		t.Fatalf("NewCostOptimized(): %v", err)
	}

	gotTier, gotReplicas, err := policy.Choose([]metrics.Metrics{{}})
	if err != nil {
		t.Fatalf("Choose(): %v", err)
	}
	if gotTier.Name() != "large" {
		t.Fatalf("tier = %q, want large", gotTier.Name())
	}
	if gotReplicas != 3 {
		t.Fatalf("replicas = %d, want min-clamped 3", gotReplicas)
	}
}

func TestCostOptimizedReturnsErrorWhenNoTierFitsMax(t *testing.T) {
	policy, err := NewCostOptimized([]tier.Tier{&fakeTier{name: "small", cost: 1}}, []replicas.Calculator{
		&fakeCalculator{byTier: map[string]int32{"small": 4}},
	}, &v1alpha1.ReplicasSpec{Min: 1, Max: 3})
	if err != nil {
		t.Fatalf("NewCostOptimized(): %v", err)
	}

	_, _, err = policy.Choose([]metrics.Metrics{{}})
	if err == nil || !strings.Contains(err.Error(), "no tier satisfies") {
		t.Fatalf("Choose() error = %v, want no tier satisfies", err)
	}
}

func TestCostOptimizedReturnsMetricErrors(t *testing.T) {
	policy, err := NewCostOptimized([]tier.Tier{&fakeTier{name: "small", cost: 1}}, []replicas.Calculator{
		&fakeCalculator{err: errors.New("boom")},
	}, nil)
	if err != nil {
		t.Fatalf("NewCostOptimized(): %v", err)
	}

	_, _, err = policy.Choose([]metrics.Metrics{{}, {}})
	if err == nil || !strings.Contains(err.Error(), `got 2 metric results, want 1`) {
		t.Fatalf("Choose() error = %v, want result count error", err)
	}

	_, _, err = policy.Choose([]metrics.Metrics{{}})
	if err == nil || !strings.Contains(err.Error(), `tier "small" metric[0]: boom`) {
		t.Fatalf("Choose() error = %v, want calculator error", err)
	}
}

func TestCostOptimizedRejectsTotalCostOverflow(t *testing.T) {
	policy, err := NewCostOptimized([]tier.Tier{&fakeTier{name: "large", cost: math.MaxInt64}}, []replicas.Calculator{
		&fakeCalculator{byTier: map[string]int32{"large": 2}},
	}, nil)
	if err != nil {
		t.Fatalf("NewCostOptimized(): %v", err)
	}

	_, _, err = policy.Choose([]metrics.Metrics{{}})
	if err == nil || !strings.Contains(err.Error(), `tier "large": total cost overflows int64`) {
		t.Fatalf("Choose() error = %v, want total cost overflow", err)
	}
}
