package nextgenmetering

import (
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/metering/pkg/common/models"
	"go.uber.org/zap"
)

func TestValidateEssentialParams(t *testing.T) {
	t.Run("missing clusterID", func(t *testing.T) {
		ok := validateEssentialParams("TestHandler", &GenerateSKUParams{TenantID: 123}, zap.NewNop())
		assert.False(t, ok)
	})

	t.Run("missing tenantID", func(t *testing.T) {
		ok := validateEssentialParams("TestHandler", &GenerateSKUParams{ClusterID: "c1"}, zap.NewNop())
		assert.False(t, ok)
	})

	t.Run("valid params", func(t *testing.T) {
		ok := validateEssentialParams("TestHandler", &GenerateSKUParams{ClusterID: "c1", TenantID: 123}, zap.NewNop())
		assert.True(t, ok)
	})
}

func TestConvertEssentialRCU(t *testing.T) {
	params := &GenerateSKUParams{
		ClusterID:     "cluster-1",
		TenantID:      6000,
		CloudProvider: "aws",
		Region:        "us-east-1",
		Plan:          models.PlanEssential,
	}

	t.Run("converts rcu all:total record", func(t *testing.T) {
		records := []models.BillingReportV2Deltalake{
			{
				Unit:   "rcu:aws:us-east-1:essential:all:total",
				Cost:   1,
				Labels: `{"intervalValue":"8000","intervalMin":"5000","intervalMax":"20000"}`,
			},
		}
		result, err := convertEssentialRCU(records, params, zap.NewNop())
		require.NoError(t, err)
		require.Len(t, result, 1)
		assert.Equal(t, "serverless-request-capacity-unit:aws:us-east-1:essential", result[0].Unit)
	})

	t.Run("skips non all:total rcu records", func(t *testing.T) {
		records := []models.BillingReportV2Deltalake{
			{Unit: "rcu:aws:us-east-1:essential:olap:total"},
		}
		result, err := convertEssentialRCU(records, params, zap.NewNop())
		require.NoError(t, err)
		assert.Len(t, result, 0)
	})

	t.Run("skips non-rcu records", func(t *testing.T) {
		records := []models.BillingReportV2Deltalake{
			{Unit: "disk-storage-size:aws:us-east-1:essential:row:standard:data:gp3"},
		}
		result, err := convertEssentialRCU(records, params, zap.NewNop())
		require.NoError(t, err)
		assert.Len(t, result, 0)
	})
}

func TestConvertEssentialStorage(t *testing.T) {
	params := &GenerateSKUParams{
		ClusterID:     "cluster-1",
		TenantID:      6000,
		CloudProvider: "aws",
		Region:        "us-east-1",
		Plan:          models.PlanEssential,
	}

	t.Run("converts row storage", func(t *testing.T) {
		records := []models.BillingReportV2Deltalake{
			{Unit: "disk-storage-size:aws:us-east-1:essential:row:standard:data:gp3", UnitValue: "2147483648"},
		}
		result := convertEssentialStorage(records, params, zap.NewNop())
		require.Len(t, result, 1)
		assert.Equal(t, "serverless-row-storage:aws:us-east-1:essential", result[0].Unit)
		assert.Equal(t, "2147483648", result[0].UnitValue)
	})

	t.Run("converts column storage", func(t *testing.T) {
		records := []models.BillingReportV2Deltalake{
			{Unit: "disk-storage-size:aws:us-east-1:essential:column:plus:data:io2", UnitValue: "536870912"},
		}
		result := convertEssentialStorage(records, params, zap.NewNop())
		require.Len(t, result, 1)
		assert.Equal(t, "serverless-column-storage:aws:us-east-1:essential", result[0].Unit)
	})

	t.Run("converts dual-encryption row storage", func(t *testing.T) {
		records := []models.BillingReportV2Deltalake{
			{Unit: "disk-storage-size-dual-encryption:aws:us-east-1:essential:row:standard:data:gp3", UnitValue: "1000"},
		}
		result := convertEssentialStorage(records, params, zap.NewNop())
		require.Len(t, result, 1)
		assert.Equal(t, "serverless-row-storage-dual-encryption:aws:us-east-1:essential", result[0].Unit)
	})

	t.Run("converts dual-encryption column storage", func(t *testing.T) {
		records := []models.BillingReportV2Deltalake{
			{Unit: "disk-storage-size-dual-encryption:aws:us-east-1:essential:column:plus:data:io2", UnitValue: "2000"},
		}
		result := convertEssentialStorage(records, params, zap.NewNop())
		require.Len(t, result, 1)
		assert.Equal(t, "serverless-column-storage-dual-encryption:aws:us-east-1:essential", result[0].Unit)
	})

	t.Run("multiple records converted independently", func(t *testing.T) {
		records := []models.BillingReportV2Deltalake{
			{Unit: "disk-storage-size:aws:us-east-1:essential:row:standard:data:gp3", UnitValue: "1000"},
			{Unit: "disk-storage-size:aws:us-east-1:essential:column:plus:data:io2", UnitValue: "2000"},
		}
		result := convertEssentialStorage(records, params, zap.NewNop())
		require.Len(t, result, 2)
		assert.Equal(t, "serverless-row-storage:aws:us-east-1:essential", result[0].Unit)
		assert.Equal(t, "serverless-column-storage:aws:us-east-1:essential", result[1].Unit)
	})

	t.Run("skips unknown layout", func(t *testing.T) {
		records := []models.BillingReportV2Deltalake{
			{Unit: "disk-storage-size:aws:us-east-1:essential:unknown:data:gp3"},
		}
		result := convertEssentialStorage(records, params, zap.NewNop())
		assert.Len(t, result, 0)
	})

	t.Run("skips non-storage records", func(t *testing.T) {
		records := []models.BillingReportV2Deltalake{
			{Unit: "rcu:aws:us-east-1:essential:all:total"},
		}
		result := convertEssentialStorage(records, params, zap.NewNop())
		assert.Len(t, result, 0)
	})

	t.Run("skips records with too few parts", func(t *testing.T) {
		records := []models.BillingReportV2Deltalake{
			{Unit: "disk-storage-size:aws:us-east-1:essential"},
		}
		result := convertEssentialStorage(records, params, zap.NewNop())
		assert.Len(t, result, 0)
	})
}

func TestConvertEssentialDataTransferOut(t *testing.T) {
	t.Run("keeps only data-transfer-out", func(t *testing.T) {
		records := []models.BillingReportV2Deltalake{
			{Unit: "data-transfer-out:aws:us-east-1:essential:proxy:public", UnitValue: "1000"},
			{Unit: "data-transfer-out:aws:us-east-1:essential:pd", UnitValue: "500"},
			{Unit: "data-transfer-cross-az:aws:us-east-1:essential:proxy", UnitValue: "200"},
		}
		result := convertEssentialDataTransferOut(records, zap.NewNop())
		require.Len(t, result, 2)
		assert.Equal(t, "data-transfer-out:aws:us-east-1:essential:proxy:public", result[0].Unit)
		assert.Equal(t, "data-transfer-out:aws:us-east-1:essential:pd", result[1].Unit)
	})

	t.Run("empty input", func(t *testing.T) {
		result := convertEssentialDataTransferOut(nil, zap.NewNop())
		assert.Len(t, result, 0)
	})
}

func TestConvertEssentialBackup(t *testing.T) {
	t.Run("keeps only backup-storage", func(t *testing.T) {
		records := []models.BillingReportV2Deltalake{
			{Unit: "backup-storage:aws:us-east-1:essential:T1", UnitValue: "1000"},
			{Unit: "backup-storage:aws:us-east-1:essential:T2", UnitValue: "500"},
			{Unit: "rcu:aws:us-east-1:essential:all:total", UnitValue: "300"},
			{Unit: "data-transfer-out:aws:us-east-1:essential:proxy", UnitValue: "200"},
		}
		result := convertEssentialBackup(records, zap.NewNop())
		require.Len(t, result, 2)
		assert.Equal(t, "backup-storage:aws:us-east-1:essential:T1", result[0].Unit)
		assert.Equal(t, "backup-storage:aws:us-east-1:essential:T2", result[1].Unit)
	})

	t.Run("empty input", func(t *testing.T) {
		result := convertEssentialBackup(nil, zap.NewNop())
		assert.Len(t, result, 0)
	})
}

func TestEssentialResourceManagerHandler_GenerateSKUs(t *testing.T) {
	gen := newTestSKUGenerator(t)
	handler := NewEssentialResourceManagerHandler(gen, zap.NewNop())
	now := time.Now()

	t.Run("validation fails without clusterID", func(t *testing.T) {
		params := &GenerateSKUParams{
			TenantID:      6000,
			CloudProvider: "aws",
			Region:        "us-east-1",
			Plan:          models.PlanEssential,
			Now:           now,
		}
		records, err := handler.GenerateSKUs(map[string]interface{}{}, params)
		require.NoError(t, err)
		assert.Empty(t, records)
	})

	t.Run("converts RCU to essential format", func(t *testing.T) {
		dataPoint := map[string]interface{}{
			models.NextGenMeteringSourceName: "pd",
			models.NextGenMeteringOLAPRU: map[string]interface{}{
				"value": uint64(5000),
				"unit":  "RU",
			},
			models.NextGenMeteringOLTPRU: map[string]interface{}{
				"value": uint64(3000),
				"unit":  "RU",
			},
		}
		enrichmentData := map[string]interface{}{
			"minRcu": "5000",
			"maxRcu": "20000",
		}
		params := &GenerateSKUParams{
			ClusterID:      "cluster-1",
			TenantID:       6000,
			Timestamp:      now.Unix(),
			CloudProvider:  "aws",
			Region:         "us-east-1",
			Plan:           models.PlanEssential,
			EnrichmentData: enrichmentData,
			Now:            now,
		}
		records, err := handler.GenerateSKUs(dataPoint, params)
		require.NoError(t, err)
		require.Len(t, records, 1)
		assert.Equal(t, "serverless-request-capacity-unit:aws:us-east-1:essential", records[0].Unit)
		assert.Contains(t, records[0].Labels, "intervalValue")
	})

	t.Run("includes backup records when backup metadata exists", func(t *testing.T) {
		dataPoint := map[string]interface{}{
			models.NextGenMeteringSourceName: "pd",
			models.NextGenMeteringOLAPRU: map[string]interface{}{
				"value": uint64(5000),
				"unit":  "RU",
			},
			models.NextGenMeteringOLTPRU: map[string]interface{}{
				"value": uint64(3000),
				"unit":  "RU",
			},
			models.NextGenMeteringIncrementalWriteByte: map[string]interface{}{
				"value": uint64(1024),
				"unit":  "bytes",
			},
		}
		enrichmentData := map[string]interface{}{
			"minRcu": "5000",
			"maxRcu": "20000",
		}
		backupMeta := map[string]interface{}{
			models.NextGenMetaBackupStorageV2: "T1",
		}
		params := &GenerateSKUParams{
			ClusterID:      "cluster-1",
			TenantID:       6000,
			Timestamp:      now.Unix(),
			CloudProvider:  "aws",
			Region:         "us-east-1",
			Plan:           models.PlanEssential,
			EnrichmentData: enrichmentData,
			BackupMetaData: backupMeta,
			Now:            now,
		}
		records, err := handler.GenerateSKUs(dataPoint, params)
		require.NoError(t, err)
		require.Len(t, records, 2)

		units := make(map[string]bool)
		for _, r := range records {
			units[r.Unit] = true
		}
		assert.True(t, units["serverless-request-capacity-unit:aws:us-east-1:essential"])
		assert.True(t, units["backup-storage:aws:us-east-1:essential:T1"])
	})
}

func TestEssentialStorageSizeHandler_GenerateSKUs(t *testing.T) {
	gen := newTestSKUGenerator(t)
	handler := NewEssentialStorageSizeHandler(gen, zap.NewNop())
	now := time.Now()

	const gib = uint64(1024 * 1024 * 1024)

	t.Run("validation fails without tenantID", func(t *testing.T) {
		params := &GenerateSKUParams{
			ClusterID:     "cluster-1",
			CloudProvider: "aws",
			Region:        "us-east-1",
			Plan:          models.PlanEssential,
			Now:           now,
		}
		records, err := handler.GenerateSKUs(map[string]interface{}{}, params)
		require.NoError(t, err)
		assert.Empty(t, records)
	})

	t.Run("converts storage to essential format", func(t *testing.T) {
		dataPoint := map[string]interface{}{
			models.NextGenMeteringSourceName: "pd",
			models.NextGenMeteringRowStorageSize: map[string]interface{}{
				"value": 2 * gib,
				"unit":  "Bytes",
			},
			models.NextGenMeteringColumnStorageSize: map[string]interface{}{
				"value": gib / 2,
				"unit":  "Bytes",
			},
		}
		enrichmentData := map[string]interface{}{
			models.NextGenMetaDiskType:           "gp3",
			models.NextGenMetaColumnDataDiskType: "io2",
		}
		params := &GenerateSKUParams{
			ClusterID:      "cluster-1",
			TenantID:       6000,
			Timestamp:      now.Unix(),
			CloudProvider:  "aws",
			Region:         "us-east-1",
			Plan:           models.PlanEssential,
			EnrichmentData: enrichmentData,
			Now:            now,
		}
		records, err := handler.GenerateSKUs(dataPoint, params)
		require.NoError(t, err)
		require.Len(t, records, 2)

		units := map[string]bool{}
		for _, r := range records {
			units[r.Unit] = true
		}
		assert.True(t, units["serverless-row-storage:aws:us-east-1:essential"])
		assert.True(t, units["serverless-column-storage:aws:us-east-1:essential"])
	})
}

func TestEssentialProxyHandler_GenerateSKUs(t *testing.T) {
	gen := newTestSKUGenerator(t)
	handler := NewEssentialProxyHandler(gen, zap.NewNop())
	now := time.Now()

	t.Run("filters data-transfer-out only", func(t *testing.T) {
		dataPoint := map[string]interface{}{
			models.NextGenMeteringSourceName: "proxy",
			models.NextGenMeteringOutBoundBytes: map[string]interface{}{
				"value": uint64(1024),
				"unit":  "bytes",
			},
		}
		params := &GenerateSKUParams{
			ClusterID:      "cluster-1",
			TenantID:       6000,
			Timestamp:      now.Unix(),
			CloudProvider:  "aws",
			Region:         "us-east-1",
			Plan:           models.PlanEssential,
			EnrichmentData: map[string]interface{}{},
			Now:            now,
		}
		records, err := handler.GenerateSKUs(dataPoint, params)
		require.NoError(t, err)
		for _, r := range records {
			assert.True(t, len(r.Unit) >= len(models.NextGenSKUDataTransferOut))
			assert.Equal(t, models.NextGenSKUDataTransferOut, r.Unit[:len(models.NextGenSKUDataTransferOut)])
		}
	})

	t.Run("validation fails without clusterID", func(t *testing.T) {
		params := &GenerateSKUParams{
			TenantID:      6000,
			CloudProvider: "aws",
			Region:        "us-east-1",
			Plan:          models.PlanEssential,
			Now:           now,
		}
		records, err := handler.GenerateSKUs(map[string]interface{}{}, params)
		require.NoError(t, err)
		assert.Empty(t, records)
	})
}

func TestEssentialDFSStatsHandler_GenerateSKUs(t *testing.T) {
	gen := newTestSKUGenerator(t)
	handler := NewEssentialDFSStatsHandler(gen, zap.NewNop())
	now := time.Now()

	t.Run("generates backup storage SKU with auto type", func(t *testing.T) {
		dataPoint := map[string]interface{}{
			models.NextGenMeteringWrittenBytes: map[string]interface{}{
				"value": uint64(1024),
				"unit":  "bytes",
			},
		}
		params := &GenerateSKUParams{
			ClusterID:      "cluster-1",
			TenantID:       6000,
			Timestamp:      now.Unix(),
			CloudProvider:  "aws",
			Region:         "us-east-1",
			Plan:           models.PlanEssential,
			EnrichmentData: map[string]interface{}{},
			Now:            now,
		}
		records, err := handler.GenerateSKUs(dataPoint, params)
		require.NoError(t, err)
		require.Len(t, records, 1)
		assert.Equal(t, "backup-storage:aws:us-east-1:essential:auto", records[0].Unit)
	})

	t.Run("validation fails without clusterID", func(t *testing.T) {
		params := &GenerateSKUParams{
			TenantID:      6000,
			CloudProvider: "aws",
			Region:        "us-east-1",
			Plan:          models.PlanEssential,
			Now:           now,
		}
		records, err := handler.GenerateSKUs(map[string]interface{}{}, params)
		require.NoError(t, err)
		assert.Empty(t, records)
	})

	t.Run("no records when written bytes is zero", func(t *testing.T) {
		dataPoint := map[string]interface{}{
			models.NextGenMeteringWrittenBytes: map[string]interface{}{
				"value": uint64(0),
				"unit":  "bytes",
			},
		}
		params := &GenerateSKUParams{
			ClusterID:      "cluster-1",
			TenantID:       6000,
			Timestamp:      now.Unix(),
			CloudProvider:  "aws",
			Region:         "us-east-1",
			Plan:           models.PlanEssential,
			EnrichmentData: map[string]interface{}{},
			Now:            now,
		}
		records, err := handler.GenerateSKUs(dataPoint, params)
		require.NoError(t, err)
		assert.Empty(t, records)
	})
}

func TestEssentialFlexBackupHandler_GenerateSKUs(t *testing.T) {
	gen := newTestSKUGenerator(t)
	handler := NewEssentialFlexBackupHandler(gen, zap.NewNop())
	now := time.Now()

	t.Run("generates backup storage SKU with daily type", func(t *testing.T) {
		dataPoint := map[string]interface{}{
			models.NextGenMeteringBackupSizeGB: map[string]interface{}{
				"value": uint64(16),
				"unit":  "GB",
			},
		}
		params := &GenerateSKUParams{
			ClusterID:      "cluster-1",
			TenantID:       6000,
			Timestamp:      now.Unix(),
			CloudProvider:  "aws",
			Region:         "us-east-1",
			Plan:           models.PlanEssential,
			EnrichmentData: map[string]interface{}{},
			Now:            now,
		}
		records, err := handler.GenerateSKUs(dataPoint, params)
		require.NoError(t, err)
		require.Len(t, records, 1)
		assert.Equal(t, "backup-storage:aws:us-east-1:essential:daily", records[0].Unit)
	})

	t.Run("validation fails without clusterID", func(t *testing.T) {
		params := &GenerateSKUParams{
			TenantID:      6000,
			CloudProvider: "aws",
			Region:        "us-east-1",
			Plan:          models.PlanEssential,
			Now:           now,
		}
		records, err := handler.GenerateSKUs(map[string]interface{}{}, params)
		require.NoError(t, err)
		assert.Empty(t, records)
	})

	t.Run("no records when backup size is zero", func(t *testing.T) {
		dataPoint := map[string]interface{}{
			models.NextGenMeteringBackupSizeGB: map[string]interface{}{
				"value": uint64(0),
				"unit":  "GB",
			},
		}
		params := &GenerateSKUParams{
			ClusterID:      "cluster-1",
			TenantID:       6000,
			Timestamp:      now.Unix(),
			CloudProvider:  "aws",
			Region:         "us-east-1",
			Plan:           models.PlanEssential,
			EnrichmentData: map[string]interface{}{},
			Now:            now,
		}
		records, err := handler.GenerateSKUs(dataPoint, params)
		require.NoError(t, err)
		assert.Empty(t, records)
	})
}

func TestEssentialManualBackupHandler_GenerateSKUs(t *testing.T) {
	gen := newTestSKUGenerator(t)
	handler := NewEssentialManualBackupHandler(gen, zap.NewNop())
	now := time.Now()

	t.Run("generates backup storage SKU with manual type", func(t *testing.T) {
		dataPoint := map[string]interface{}{
			models.NextGenMeteringBackupSizeGB: map[string]interface{}{
				"value": uint64(16),
				"unit":  "GB",
			},
		}
		params := &GenerateSKUParams{
			ClusterID:      "cluster-1",
			TenantID:       6000,
			Timestamp:      now.Unix(),
			CloudProvider:  "aws",
			Region:         "us-east-1",
			Plan:           models.PlanEssential,
			EnrichmentData: map[string]interface{}{},
			Now:            now,
		}
		records, err := handler.GenerateSKUs(dataPoint, params)
		require.NoError(t, err)
		require.Len(t, records, 1)
		assert.Equal(t, "backup-storage:aws:us-east-1:essential:manual", records[0].Unit)
	})

	t.Run("validation fails without clusterID", func(t *testing.T) {
		params := &GenerateSKUParams{
			TenantID:      6000,
			CloudProvider: "aws",
			Region:        "us-east-1",
			Plan:          models.PlanEssential,
			Now:           now,
		}
		records, err := handler.GenerateSKUs(map[string]interface{}{}, params)
		require.NoError(t, err)
		assert.Empty(t, records)
	})

	t.Run("no records when backup size is zero", func(t *testing.T) {
		dataPoint := map[string]interface{}{
			models.NextGenMeteringBackupSizeGB: map[string]interface{}{
				"value": uint64(0),
				"unit":  "GB",
			},
		}
		params := &GenerateSKUParams{
			ClusterID:      "cluster-1",
			TenantID:       6000,
			Timestamp:      now.Unix(),
			CloudProvider:  "aws",
			Region:         "us-east-1",
			Plan:           models.PlanEssential,
			EnrichmentData: map[string]interface{}{},
			Now:            now,
		}
		records, err := handler.GenerateSKUs(dataPoint, params)
		require.NoError(t, err)
		assert.Empty(t, records)
	})
}
