package impl

import (
	"testing"
	"time"

	"github.com/pingcap/dbaas/billing2/constant"
	"github.com/pingcap/dbaas/billing2/db/entity"
	"github.com/stretchr/testify/assert"
)

// testHashDate is 2026-08-28T00:00:00Z (unix 1787875200), a day boundary so the
// legacy hash domain (day granularity) and the replay full-timestamp domain are
// visibly distinct.
var testHashDate = time.Date(2026, 8, 28, 0, 0, 0, 0, time.UTC)

func TestHistoryIsComplete(t *testing.T) {
	tests := []struct {
		name     string
		cost     entity.DateUnitCostSummary
		expected bool
	}{
		{
			name: "two entries covering the row cost",
			cost: entity.DateUnitCostSummary{
				UnitCostSummary: entity.UnitCostSummary{
					Cost:                150,
					MergedLabelsHistory: entity.MergedLabelsHistory{{Cost: 100}, {Cost: 50}},
				},
			},
			expected: true,
		},
		{
			name: "single entry covering the row cost",
			cost: entity.DateUnitCostSummary{
				UnitCostSummary: entity.UnitCostSummary{
					Cost:                150,
					MergedLabelsHistory: entity.MergedLabelsHistory{{Cost: 150}},
				},
			},
			expected: true,
		},
		{
			name: "empty history falls back",
			cost: entity.DateUnitCostSummary{
				UnitCostSummary: entity.UnitCostSummary{Cost: 150},
			},
			expected: false,
		},
		{
			name: "flag toggled off mid-row: missing tail contributions",
			cost: entity.DateUnitCostSummary{
				UnitCostSummary: entity.UnitCostSummary{
					Cost:                150,
					MergedLabelsHistory: entity.MergedLabelsHistory{{Cost: 100}},
				},
			},
			expected: false,
		},
		{
			name: "defensive: history exceeding the row cost falls back",
			cost: entity.DateUnitCostSummary{
				UnitCostSummary: entity.UnitCostSummary{
					Cost:                150,
					MergedLabelsHistory: entity.MergedLabelsHistory{{Cost: 100}, {Cost: 100}},
				},
			},
			expected: false,
		},
		{
			name: "zero-cost row with zero entries is complete",
			cost: entity.DateUnitCostSummary{
				UnitCostSummary: entity.UnitCostSummary{
					Cost:                0,
					MergedLabelsHistory: entity.MergedLabelsHistory{{Cost: 0}, {Cost: 0}},
				},
			},
			expected: true,
		},
		{
			name: "negative costs covering the row cost",
			cost: entity.DateUnitCostSummary{
				UnitCostSummary: entity.UnitCostSummary{
					Cost:                -150,
					MergedLabelsHistory: entity.MergedLabelsHistory{{Cost: -100}, {Cost: -50}},
				},
			},
			expected: true,
		},
		{
			name: "negative costs not covering the row cost",
			cost: entity.DateUnitCostSummary{
				UnitCostSummary: entity.UnitCostSummary{
					Cost:                -50,
					MergedLabelsHistory: entity.MergedLabelsHistory{{Cost: -100}},
				},
			},
			expected: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert.Equal(t, tt.expected, historyIsComplete(tt.cost))
		})
	}
}

func TestBuildTransferMeasureLegacyFallbackCostID(t *testing.T) {
	labels := map[string]string{"key1": "v1", "key2": "v2"}
	measure := buildTransferMeasure(1001, constant.MSPCustomerIDLabel, "789", "unit-1", 42, testHashDate, labels, "")

	// The fallback costID must stay byte-identical with the pre-feature transfer:
	// md5 over sorted key+value pairs ("MSPCustomerID789" + "key1v1" + "key2v2"),
	// then unit, cost, day unix — no extra bytes. Expected md5 computed
	// independently from the concatenated input, not from the code under test.
	assert.Equal(t, "1001-6ec1e12133e0b9e27a5bc902ad428c73", measure.CostID)
	assert.Equal(t, int64(1787875200), measure.CostTimeStamp)
	assert.Equal(t, "unit-1", measure.Unit)
	assert.Equal(t, 42, measure.Cost)
	assert.Equal(t, map[string]string{
		"key1":                      "v1",
		"key2":                      "v2",
		constant.MSPCustomerIDLabel: "789",
	}, measure.Labels)

	// Defensive copy: the caller's labels map is not mutated.
	assert.Len(t, labels, 2)
	assert.Equal(t, "v1", labels["key1"])
	assert.Equal(t, "v2", labels["key2"])
}

func TestBuildTransferMeasureReplayDomain(t *testing.T) {
	entryDate := testHashDate.Add(time.Hour) // 2026-08-28T01:00:00Z, unix 1787878800
	labels := map[string]string{"intervalValue": "1"}

	m1 := buildTransferMeasure(1001, constant.MSPCustomerIDLabel, "789", "unit-1", 42, entryDate, labels, "raw-cost-1")
	// md5 over "MSPCustomerID789" + "intervalValue1" + "unit-1" + "42" +
	// "1787878800" + "raw-cost-1", computed independently.
	assert.Equal(t, "1001-a4e66db66fe2eda6ef460c787961112b", m1.CostID)
	assert.Equal(t, int64(1787878800), m1.CostTimeStamp)
	assert.Equal(t, 42, m1.Cost)
	assert.Equal(t, map[string]string{
		"intervalValue":             "1",
		constant.MSPCustomerIDLabel: "789",
	}, m1.Labels)

	// Deterministic: identical inputs yield the identical costID (retry idempotency).
	m1Again := buildTransferMeasure(1001, constant.MSPCustomerIDLabel, "789", "unit-1", 42, entryDate,
		map[string]string{"intervalValue": "1"}, "raw-cost-1")
	assert.Equal(t, m1.CostID, m1Again.CostID)

	// Same unit/labels/cost/timestamp but a different source raw cost must not collide.
	m2 := buildTransferMeasure(1001, constant.MSPCustomerIDLabel, "789", "unit-1", 42, entryDate, labels, "raw-cost-2")
	assert.Equal(t, "1001-f7bd8a011fd051bba1436a6df1c42934", m2.CostID)
	assert.NotEqual(t, m1.CostID, m2.CostID)

	// Same source raw cost at a different timestamp must not collide either.
	m3 := buildTransferMeasure(1001, constant.MSPCustomerIDLabel, "789", "unit-1", 42, entryDate.Add(time.Hour), labels, "raw-cost-1")
	assert.NotEqual(t, m1.CostID, m3.CostID)

	// The replay domain never collides with the legacy fallback domain.
	legacy := buildTransferMeasure(1001, constant.MSPCustomerIDLabel, "789", "unit-1", 42, testHashDate, labels, "")
	assert.NotEqual(t, m1.CostID, legacy.CostID)
}
