package server

import (
	"testing"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	dto "github.com/prometheus/client_model/go"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	apiv1 "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	"google.golang.org/grpc/codes"
	"google.golang.org/protobuf/types/known/wrapperspb"
)

// TestPolicyWriteMetricLabels_PinCardinality pins the label set
// for autoscale_policy_write_total. The design doc §Metrics
// mandates a small label set with bounded cardinality; this test
// exercises the helper functions that translate proto types into
// labels so a future enum-extension does not silently introduce
// unbounded labels.
//
// Cardinality budget per design doc: 4 scopes x 3 operations x
// 3 apply_to_all values x ~5 results = <=180 series total.
func TestPolicyWriteMetricLabels_PinCardinality(t *testing.T) {
	t.Run("applyToAllLabel collapses nil/false to false", func(t *testing.T) {
		assert.Equal(t, "false", applyToAllLabel(nil))
		assert.Equal(t, "false", applyToAllLabel(wrapperspb.Bool(false)))
		assert.Equal(t, "true", applyToAllLabel(wrapperspb.Bool(true)))
	})

	t.Run("policyWriteOperationLabel covers all three operations", func(t *testing.T) {
		assert.Equal(t, "create_or_replace", policyWriteOperationLabel(operationCreateOrReplace))
		assert.Equal(t, "patch", policyWriteOperationLabel(operationPatch))
		assert.Equal(t, "sync", policyWriteOperationLabel(operationSync))
		// Unknown / zero value → "unknown", never panics.
		assert.Equal(t, "unknown", policyWriteOperationLabel(policyOperation(99)))
	})

	t.Run("fanoutStatusLabel covers all three statuses", func(t *testing.T) {
		assert.Equal(t, "synced", fanoutStatusLabel(apiv1.AutoscalePolicyApplyStatus_APPLY_STATUS_SYNCED))
		assert.Equal(t, "failed", fanoutStatusLabel(apiv1.AutoscalePolicyApplyStatus_APPLY_STATUS_FAILED))
		assert.Equal(t, "skipped", fanoutStatusLabel(apiv1.AutoscalePolicyApplyStatus_APPLY_STATUS_SKIPPED))
		// Unspecified / unknown → "unknown", never panics.
		assert.Equal(t, "unknown", fanoutStatusLabel(apiv1.AutoscalePolicyApplyStatus_AUTOSCALE_POLICY_APPLY_STATUS_UNSPECIFIED))
	})

	t.Run("statusCodeToPolicyWriteResult covers OK + InvalidArgument + Unimplemented + others", func(t *testing.T) {
		assert.Equal(t, policyWriteResultOK, statusCodeToPolicyWriteResult(codes.OK))
		assert.Equal(t, policyWriteResultInvalidArg, statusCodeToPolicyWriteResult(codes.InvalidArgument))
		assert.Equal(t, policyWriteResultUnimplemented, statusCodeToPolicyWriteResult(codes.Unimplemented))
		// Any other code (Internal, FailedPrecondition, ...) collapses
		// to internal_error so a future code addition never explodes
		// the label cardinality.
		assert.Equal(t, policyWriteResultInternalError, statusCodeToPolicyWriteResult(codes.Internal))
		assert.Equal(t, policyWriteResultInternalError, statusCodeToPolicyWriteResult(codes.FailedPrecondition))
	})
}

// TestMarkPolicyWrite_IncrementsCounter pins that markPolicyWrite
// increments autoscale_policy_write_total with the expected labels
// (scope, operation, apply_to_all, result). We use the
// testutil.ToFloat64 pattern to read the counter value back out.
func TestMarkPolicyWrite_IncrementsCounter(t *testing.T) {
	// Reset only this counter's known series so we don't interfere
	// with other tests. We do not delete by-name on the package
	// variable directly; instead we read the current value, run
	// markPolicyWrite, and assert the delta.
	before := readCounter(t, autoscalePolicyWriteTotal, prometheus.Labels{
		"scope":        apiv1.AutoscalePolicyScope_SCOPE_TENANT.String(),
		"operation":    "create_or_replace",
		"apply_to_all": "true",
		"result":       policyWriteResultOK,
	})

	markPolicyWrite(
		apiv1.AutoscalePolicyScope_SCOPE_TENANT,
		operationCreateOrReplace,
		wrapperspb.Bool(true),
		policyWriteResultOK,
		zeroTime,
	)

	after := readCounter(t, autoscalePolicyWriteTotal, prometheus.Labels{
		"scope":        apiv1.AutoscalePolicyScope_SCOPE_TENANT.String(),
		"operation":    "create_or_replace",
		"apply_to_all": "true",
		"result":       policyWriteResultOK,
	})
	assert.InDelta(t, before+1, after, 0,
		"markPolicyWrite must increment write_total by exactly 1 for the given label set")
}

// TestMarkPolicyWrite_InvalidArgumentResult pins that a rejected
// request (InvalidArgument from nil state, wrong scope, etc.)
// shows up as result=invalid_argument in the counter, not as ok.
// This is the dashboard signal operators use to distinguish
// "operator typo" from "Internal failure".
func TestMarkPolicyWrite_InvalidArgumentResult(t *testing.T) {
	before := readCounter(t, autoscalePolicyWriteTotal, prometheus.Labels{
		"scope":        apiv1.AutoscalePolicyScope_SCOPE_SHARED_POOL.String(),
		"operation":    "sync",
		"apply_to_all": "false",
		"result":       policyWriteResultInvalidArg,
	})

	markPolicyWrite(
		apiv1.AutoscalePolicyScope_SCOPE_SHARED_POOL,
		operationSync,
		nil,
		policyWriteResultInvalidArg,
		zeroTime,
	)

	after := readCounter(t, autoscalePolicyWriteTotal, prometheus.Labels{
		"scope":        apiv1.AutoscalePolicyScope_SCOPE_SHARED_POOL.String(),
		"operation":    "sync",
		"apply_to_all": "false",
		"result":       policyWriteResultInvalidArg,
	})
	assert.InDelta(t, before+1, after, 0,
		"rejected SyncAutoscalePolicy at SHARED_POOL scope must increment write_total{result=invalid_argument} by exactly 1")
}

// TestMarkPolicyFanoutTarget_IncrementsCounter pins the per-target
// fanout counter (no tenant_id label — fixed cardinality).
func TestMarkPolicyFanoutTarget_IncrementsCounter(t *testing.T) {
	beforeSynced := readCounter(t, autoscalePolicyFanoutTargetsTotal, prometheus.Labels{
		"operation": "patch",
		"status":    "synced",
	})
	beforeFailed := readCounter(t, autoscalePolicyFanoutTargetsTotal, prometheus.Labels{
		"operation": "patch",
		"status":    "failed",
	})

	markPolicyFanoutTarget(operationPatch,
		apiv1.AutoscalePolicyApplyStatus_APPLY_STATUS_SYNCED)
	markPolicyFanoutTarget(operationPatch,
		apiv1.AutoscalePolicyApplyStatus_APPLY_STATUS_FAILED)

	afterSynced := readCounter(t, autoscalePolicyFanoutTargetsTotal, prometheus.Labels{
		"operation": "patch",
		"status":    "synced",
	})
	afterFailed := readCounter(t, autoscalePolicyFanoutTargetsTotal, prometheus.Labels{
		"operation": "patch",
		"status":    "failed",
	})

	assert.InDelta(t, beforeSynced+1, afterSynced, 0,
		"synced fanout target must increment fanout_targets_total{status=synced} by 1")
	assert.InDelta(t, beforeFailed+1, afterFailed, 0,
		"failed fanout target must increment fanout_targets_total{status=failed} by 1")
}

// TestObservePolicyFanoutDuration_DoesNotPanic pins that the
// duration helper is robust to a zero time.Time (i.e. caller
// forgot to capture startedAt). Observe on a zero elapsed-time
// would otherwise produce a 0-second bucket that pollutes
// dashboards; the helper's IsZero guard returns early.
func TestObservePolicyFanoutDuration_DoesNotPanic(t *testing.T) {
	observePolicyFanoutDuration(operationCreateOrReplace, zeroTime)
	observePolicyFanoutDuration(operationPatch, zeroTime)
	observePolicyFanoutDuration(operationSync, zeroTime)
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

// zeroTime is a stable sentinel for the "no startedAt" guard test.
var zeroTime = (time.Time{})

// readCounter reads the current counter value for a metric with the
// given label set. Returns 0 if the series does not exist (which is
// the desired behavior for "before" deltas in a test).
func readCounter(t *testing.T, c *prometheus.CounterVec, labels prometheus.Labels) float64 {
	t.Helper()
	// Build a metric whose labels match the requested set. CounterVec
	// returns a no-op counter when no series exists, so a missing
	// series is safe.
	m, err := c.GetMetricWith(labels)
	require.NoError(t, err, "GetMetricWith must accept the label set")
	pb := &dto.Metric{}
	require.NoError(t, m.Write(pb))
	if pb.Counter == nil {
		return 0
	}
	return pb.Counter.GetValue()
}
