package models

import (
	"encoding/json"
	"fmt"
	"strconv"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestConvert2OldReporter(t *testing.T) {
	tests := []struct {
		name    string
		input   BillingReportV2
		want    BillingReport
		wantErr bool
	}{
		{
			name: "node case",
			input: BillingReportV2{
				TenantID:      1,
				CostID:        "test-1",
				Cost:          100,
				CostTimestamp: 1234567890,
				Unit:          "node",
				UnitValue:     "1",
				Labels:        `{"key":"value"}`,
			},
			want: BillingReport{
				TenantID:      1,
				CostID:        "test-1",
				Cost:          100,
				CostTimestamp: 1234567890,
				Unit:          "node",
				Labels:        `{"key":"value"}`,
			},
			wantErr: false,
		},
		{
			name: "tiproxy case",
			input: BillingReportV2{
				TenantID:      1,
				CostID:        "test-1",
				Cost:          100,
				CostTimestamp: 1234567890,
				Unit:          "tiproxy:aws:us-west-2:DEPLOY_TYPE_DEDICATED:m5.xlarge",
				UnitValue:     "1",
				Labels:        `{"key":"value"}`,
			},
			want: BillingReport{
				TenantID:      1,
				CostID:        "test-1",
				Cost:          100,
				CostTimestamp: 1234567890,
				Unit:          "tiproxy:aws:us-west-2:DEPLOY_TYPE_DEDICATED:m5.xlarge",
				Labels:        `{"key":"value"}`,
			},
			wantErr: false,
		},
		{
			name: "disk-storage case",
			input: BillingReportV2{
				TenantID:      1,
				CostID:        "test-2",
				Cost:          10,
				CostTimestamp: 1234567890,
				Unit:          "disk-storage",
				UnitValue:     "100",
				Labels:        `{"key":"value"}`,
			},
			want: BillingReport{
				TenantID:      1,
				CostID:        "test-2",
				Cost:          1000, // 100 * 10
				CostTimestamp: 1234567890,
				Unit:          "disk-storage",
				Labels:        `{"key":"value"}`,
			},
			wantErr: false,
		},
		{
			name: "disk-iops case",
			input: BillingReportV2{
				TenantID:      1,
				CostID:        "test-3",
				Cost:          100,
				CostTimestamp: 1234567890,
				Unit:          "disk-iops",
				UnitValue:     "3000",
				Labels:        `{"key":"value"}`,
			},
			want: BillingReport{
				TenantID:      1,
				CostID:        "test-3",
				Cost:          100,
				CostTimestamp: 1234567890,
				Unit:          "disk-iops",
				Labels:        `{"iops":"3000","key":"value"}`,
			},
			wantErr: false,
		},
		{
			name: "disk-throughput case",
			input: BillingReportV2{
				TenantID:      1,
				CostID:        "test-4",
				Cost:          100,
				CostTimestamp: 1234567890,
				Unit:          "disk-throughput",
				UnitValue:     "125",
				Labels:        `{"key":"value"}`,
			},
			want: BillingReport{
				TenantID:      1,
				CostID:        "test-4",
				Cost:          100,
				CostTimestamp: 1234567890,
				Unit:          "disk-throughput",
				Labels:        `{"key":"value","throughput":"125"}`,
			},
			wantErr: false,
		},
		{
			name: "disk-additional-iops case",
			input: BillingReportV2{
				TenantID:      1,
				CostID:        "test-5",
				Cost:          100,
				CostTimestamp: 1234567890,
				Unit:          "disk-additional-iops",
				UnitValue:     "1000",
				Labels:        `{"key":"value"}`,
			},
			want: BillingReport{
				TenantID:      1,
				CostID:        "test-5",
				Cost:          100000,
				CostTimestamp: 1234567890,
				Unit:          "disk-additional-iops",
				Labels:        `{"key":"value"}`,
			},
			wantErr: false,
		},
		{
			name: "invalid unit case",
			input: BillingReportV2{
				Unit: "invalid",
			},
			wantErr: true,
		},
		{
			name: "invalid storage value",
			input: BillingReportV2{
				Unit:      "disk-storage",
				UnitValue: "invalid",
			},
			wantErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := tt.input.Convert2OldReporter()
			if (err != nil) != tt.wantErr {
				t.Errorf("Convert2OldReporter() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if !tt.wantErr && got != tt.want {
				t.Errorf("Convert2OldReporter() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestBillingReportV2Deltalake_Conversion(t *testing.T) {
	billingdelta := BillingReportV2Deltalake{
		TenantID:      12345,
		CostID:        "cost-001",
		Cost:          500,
		CostTimestamp: 1696543200,
		Unit:          "node",
		UnitValue:     "2",
		Labels:        `{"env":"production","region":"us-west-2"}`,
		CreatedAt:     1696546800,
		SKUType:       NextGenSKUTypePrivate,
		HourDimension: "2025092418",
	}
	billingv2 := billingdelta.Convert2BillingReportV2()
	assert.Equal(t, billingdelta.TenantID, billingv2.TenantID)
	assert.Equal(t, billingdelta.CostID, billingv2.CostID)
	assert.Equal(t, billingdelta.Cost, billingv2.Cost)
	assert.Equal(t, billingdelta.CostTimestamp, billingv2.CostTimestamp)
	assert.Equal(t, billingdelta.Unit, billingv2.Unit)
	assert.Equal(t, billingdelta.UnitValue, billingv2.UnitValue)
	assert.Equal(t, billingdelta.Labels, billingv2.Labels)
	assert.Equal(t, billingdelta.CreatedAt, billingv2.CreatedAt)
	billingv1, err := billingdelta.Convert2BillingReport()
	assert.NoError(t, err)
	univ, err := strconv.ParseInt(billingdelta.UnitValue, 10, 64)
	assert.NoError(t, err)
	assert.Equal(t, univ, int64(2))
	billingv2cost := billingdelta.Cost * univ
	assert.Equal(t, billingdelta.TenantID, billingv1.TenantID)
	assert.Equal(t, billingdelta.CostID, billingv1.CostID)
	assert.Equal(t, billingv2cost, billingv1.Cost)
	assert.Equal(t, billingdelta.CostTimestamp, billingv1.CostTimestamp)
	assert.Equal(t, billingdelta.Unit, billingv1.Unit)
	assert.Equal(t, billingdelta.Labels, billingv1.Labels)
}

func TestBillingReportV2Deltalake_ConvertBytesSkusToMiB(t *testing.T) {
	const (
		baseCost  int64  = 2
		timestamp        = 1696543200
		tenantID  uint64 = 12345
	)

	tests := []struct {
		name          string
		unit          string
		bytes         int64
		initialLabels string
		expectedMiB   int64
	}{
		{
			name:          "data transfer out exact MiB",
			unit:          NextGenSKUDataTransferOut,
			bytes:         5 * 1024 * 1024,
			initialLabels: `{"sourceUnit":"bytes"}`,
			expectedMiB:   5,
		},
		{
			name:          "data transfer cross az round up",
			unit:          NextGenSKUDataTransferCrossAZ + ":aws:us-west-2:premium:proxy",
			bytes:         3*1024*1024 + 100,
			initialLabels: `{"sourceUnit":"bytes","clusterID":"c-1"}`,
			expectedMiB:   4,
		},
		{
			name:        "backup storage minimum 1 MiB",
			unit:        NextGenSKUBackupStorage,
			bytes:       512 * 1024,
			expectedMiB: 1,
		},
		{
			name:          "disk storage size zero bytes",
			unit:          NextGenSKUDiskStorageSize + ":aws:us-west-2:premium:row:standard:data:unknown",
			bytes:         0,
			initialLabels: `{"layout":"row"}`,
			expectedMiB:   1,
		},
		{
			name:          "dual encryption disk storage converts to MiB",
			unit:          NextGenSKUDiskStorageSizeDualEncryption + ":aws:ap-northeast-1:premium:row:standard:data:gp3",
			bytes:         2*1024*1024 + 1,
			initialLabels: `{"sourceUnit":"bytes"}`,
			expectedMiB:   3,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			report := BillingReportV2Deltalake{
				SchemeVersion: "v2",
				TenantID:      tenantID,
				CostID:        "cost-123",
				Cost:          baseCost,
				CostTimestamp: timestamp,
				Unit:          tt.unit,
				UnitValue:     strconv.FormatInt(tt.bytes, 10),
				Labels:        tt.initialLabels,
			}

			billingReport, err := report.Convert2BillingReport()
			assert.NoError(t, err)
			assert.Equal(t, baseCost*tt.expectedMiB, billingReport.Cost)

			var labels map[string]string
			if billingReport.Labels != "" {
				err = json.Unmarshal([]byte(billingReport.Labels), &labels)
				assert.NoError(t, err)
			} else {
				labels = map[string]string{}
			}
			assert.Equal(t, strconv.FormatInt(tt.bytes, 10), labels[NextGenSKULabelKeyOriginalBytes])

			if strings.TrimSpace(tt.initialLabels) != "" {
				var original map[string]string
				err = json.Unmarshal([]byte(tt.initialLabels), &original)
				assert.NoError(t, err)
				for key, value := range original {
					assert.Equal(t, value, labels[key])
				}
			}
		})
	}
}

func TestBillingReportV2Deltalake_ConvertManualBackupStorageAsGB(t *testing.T) {
	report := BillingReportV2Deltalake{
		SchemeVersion: "v2",
		TenantID:      12345,
		CostID:        "manual-backup-cost",
		Cost:          2,
		CostTimestamp: 1696543200,
		Unit:          NextGenSKUBackupStorage + ":aws:ap-south-1:premium:manual",
		UnitValue:     "128",
		Labels:        `{"sourceUnit":"gb","backupType":"manual","backupID":"backup-1"}`,
	}

	billingReport, err := report.Convert2BillingReport()

	require.NoError(t, err)
	assert.Equal(t, int64(256), billingReport.Cost)
	assert.Equal(t, report.Unit, billingReport.Unit)
	assert.Equal(t, report.Labels, billingReport.Labels)

	var labels map[string]string
	require.NoError(t, json.Unmarshal([]byte(billingReport.Labels), &labels))
	assert.Equal(t, "gb", labels[NextGenSKULabelKeySourceUnit])
	assert.Equal(t, "manual", labels[NextGenSKULabelKeyBackupType])
	assert.NotContains(t, labels, NextGenSKULabelKeyOriginalBytes)
}

func TestBillingReportV2Deltalake_ConvertDiskMetrics(t *testing.T) {
	base := BillingReportV2Deltalake{
		SchemeVersion: "v2",
		TenantID:      42,
		CostID:        "disk-metric",
		Cost:          123,
		CostTimestamp: 1700000000,
	}

	t.Run("disk iops keeps cost and adds label", func(t *testing.T) {
		report := base
		report.Unit = NextGenSKUDiskIOPS + ":aws:us-west-2:premium:tikv:standard:data:gp3"
		report.UnitValue = "4000"
		report.Labels = `{"existing":"label"}`

		billing, err := report.Convert2BillingReport()
		assert.NoError(t, err)
		assert.Equal(t, report.Cost, billing.Cost)
		assert.Equal(t, report.Unit, billing.Unit)
		assert.Equal(t, report.TenantID, billing.TenantID)

		var labels map[string]string
		err = json.Unmarshal([]byte(billing.Labels), &labels)
		assert.NoError(t, err)
		assert.Equal(t, "label", labels["existing"])
		assert.Equal(t, report.UnitValue, labels["iops"])
	})

	t.Run("disk throughput keeps cost and adds label", func(t *testing.T) {
		report := base
		report.Unit = NextGenSKUDiskThroughput + ":aws:us-west-2:premium:tikv:standard:data:gp3"
		report.UnitValue = "125"
		report.Labels = `{"sourceUnit":"mb/s"}`

		billing, err := report.Convert2BillingReport()
		assert.NoError(t, err)
		assert.Equal(t, report.Cost, billing.Cost)
		assert.Equal(t, report.Unit, billing.Unit)
		assert.Equal(t, report.TenantID, billing.TenantID)

		var labels map[string]string
		err = json.Unmarshal([]byte(billing.Labels), &labels)
		assert.NoError(t, err)
		assert.Equal(t, "mb/s", labels["sourceUnit"])
		assert.Equal(t, report.UnitValue, labels["throughput"])
	})
}

func TestBillingReportV2Deltalake_ConvertRCU(t *testing.T) {
	base := BillingReportV2Deltalake{
		SchemeVersion: "v2",
		TenantID:      99,
		CostID:        "rcu-metric",
		Cost:          7,
		CostTimestamp: 1700001000,
		Unit:          NextGenSKURCU + ":aws:us-west-2:premium:all:total",
		UnitValue:     "1",
	}

	t.Run("interval value rewritten with ceiling minutes", func(t *testing.T) {
		report := base
		report.Labels = fmt.Sprintf(`{"%s":"180","extra":"label"}`, NextGenSKULabelKeyIntervalValue)
		billing, err := report.Convert2BillingReport()
		assert.NoError(t, err)
		assert.Equal(t, report.Cost, billing.Cost)
		var labels map[string]string
		err = json.Unmarshal([]byte(billing.Labels), &labels)
		assert.NoError(t, err)
		assert.Equal(t, "3", labels[NextGenSKULabelKeyIntervalValue])
		assert.Equal(t, "label", labels["extra"])
	})

	t.Run("interval with remainder rounds up", func(t *testing.T) {
		report := base
		report.Labels = fmt.Sprintf(`{"%s":"125"}`, NextGenSKULabelKeyIntervalValue)
		billing, err := report.Convert2BillingReport()
		assert.NoError(t, err)
		assert.Equal(t, report.Cost, billing.Cost)
		var labels map[string]string
		err = json.Unmarshal([]byte(billing.Labels), &labels)
		assert.NoError(t, err)
		assert.Equal(t, "3", labels[NextGenSKULabelKeyIntervalValue])
	})

	t.Run("missing interval produces error", func(t *testing.T) {
		report := base
		report.Labels = `{"other":"value"}`
		_, err := report.Convert2BillingReport()
		assert.Error(t, err)
	})

	t.Run("invalid interval produces error", func(t *testing.T) {
		report := base
		report.Labels = fmt.Sprintf(`{"%s":"abc"}`, NextGenSKULabelKeyIntervalValue)
		_, err := report.Convert2BillingReport()
		assert.Error(t, err)
	})

	t.Run("negative interval produces error", func(t *testing.T) {
		report := base
		report.Labels = fmt.Sprintf(`{"%s":"-1"}`, NextGenSKULabelKeyIntervalValue)
		_, err := report.Convert2BillingReport()
		assert.Error(t, err)
	})
}

func TestBillingReportV2Deltalake_ConvertServerlessRCU(t *testing.T) {
	base := BillingReportV2Deltalake{
		SchemeVersion: "v2",
		TenantID:      99,
		CostID:        "srcu-metric",
		Cost:          1,
		CostTimestamp: 1700001000,
		Unit:          NextGenSKUServerlessRequestCapacityUnit + ":aws:us-east-1:essential",
		UnitValue:     "1",
	}

	t.Run("clamps with RU to RCU conversion and intervalMin", func(t *testing.T) {
		report := base
		report.Labels = fmt.Sprintf(`{"%s":"3000","%s":"100"}`, NextGenSKULabelKeyIntervalValue, NextGenSKULabelKeyIntervalMin)
		billing, err := report.Convert2BillingReport()
		require.NoError(t, err)
		var labels map[string]string
		err = json.Unmarshal([]byte(billing.Labels), &labels)
		require.NoError(t, err)
		assert.Equal(t, "100", labels[NextGenSKULabelKeyIntervalValue])
		assert.Equal(t, int64(100), billing.Cost)
	})

	t.Run("clamps with intervalMax", func(t *testing.T) {
		report := base
		report.Labels = fmt.Sprintf(`{"%s":"120000","%s":"100","%s":"500"}`, NextGenSKULabelKeyIntervalValue, NextGenSKULabelKeyIntervalMin, NextGenSKULabelKeyIntervalMax)
		billing, err := report.Convert2BillingReport()
		require.NoError(t, err)
		var labels map[string]string
		err = json.Unmarshal([]byte(billing.Labels), &labels)
		require.NoError(t, err)
		assert.Equal(t, "500", labels[NextGenSKULabelKeyIntervalValue])
		assert.Equal(t, int64(500), billing.Cost)
	})

	t.Run("no clamping when value in range", func(t *testing.T) {
		report := base
		report.Labels = fmt.Sprintf(`{"%s":"6000","%s":"50","%s":"200"}`, NextGenSKULabelKeyIntervalValue, NextGenSKULabelKeyIntervalMin, NextGenSKULabelKeyIntervalMax)
		billing, err := report.Convert2BillingReport()
		require.NoError(t, err)
		var labels map[string]string
		err = json.Unmarshal([]byte(billing.Labels), &labels)
		require.NoError(t, err)
		assert.Equal(t, "100", labels[NextGenSKULabelKeyIntervalValue])
		assert.Equal(t, int64(100), billing.Cost)
	})

	t.Run("missing labels returns error", func(t *testing.T) {
		report := base
		report.Labels = ""
		_, err := report.Convert2BillingReport()
		assert.Error(t, err)
	})

	t.Run("missing intervalValue returns error", func(t *testing.T) {
		report := base
		report.Labels = fmt.Sprintf(`{"%s":"100"}`, NextGenSKULabelKeyIntervalMin)
		_, err := report.Convert2BillingReport()
		assert.Error(t, err)
	})
}

func TestBillingReport_Getters(t *testing.T) {
	b := BillingReport{TenantID: 42}
	assert.Equal(t, uint64(42), b.GetTenantID())
}

func TestBillingReport_IsNodeSKU(t *testing.T) {
	assert.True(t, BillingReport{Unit: "node:m5.xlarge"}.IsNodeSKU())
	assert.False(t, BillingReport{Unit: "rcu:aws:us-west-2:premium:all:total"}.IsNodeSKU())
}

func TestBillingReport_IsTiproxySKU(t *testing.T) {
	assert.True(t, BillingReport{Unit: "tiproxy:aws:us-west-2:dedicated:m5.xlarge"}.IsTiproxySKU())
	assert.False(t, BillingReport{Unit: "node:m5.xlarge"}.IsTiproxySKU())
}

func TestBillingReportV2_Getters(t *testing.T) {
	b := BillingReportV2{TenantID: 99}
	assert.Equal(t, uint64(99), b.GetTenantID())
}

func TestBillingReportV2_IsTiproxySKU(t *testing.T) {
	assert.True(t, BillingReportV2{Unit: "tiproxy:aws:us-west-2:dedicated:m5.xlarge"}.IsTiproxySKU())
	assert.False(t, BillingReportV2{Unit: "rcu:aws:us-west-2:premium:all:total"}.IsTiproxySKU())
}

func TestBillingReportV2Deltalake_GetTenantID(t *testing.T) {
	b := BillingReportV2Deltalake{TenantID: 123}
	assert.Equal(t, uint64(123), b.GetTenantID())
}
