package dashboard

import (
	"context"
	"database/sql/driver"
	"math"
	"testing"
	"time"

	"github.com/DATA-DOG/go-sqlmock"
	"hyapp-admin-server/internal/config"
)

func TestMySQLExternalDashboardSourceStatisticsOverview(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	defer db.Close()

	source := NewMySQLExternalDashboardSource(db, config.DashboardExternalSourceConfig{
		Enabled:        true,
		AppCode:        "Yumi",
		AppName:        "Yumi",
		SysOrigin:      "LIKEI",
		StatTimezone:   "Asia/Riyadh",
		RequestTimeout: time.Second,
	})
	if source == nil {
		t.Fatal("source is nil")
	}

	refreshedAt := time.Date(2026, 6, 30, 23, 0, 0, 0, time.UTC)
	current := externalMetricRow(refreshedAt,
		7, 40, 5,
		"25.00", "100.00", "20.00", "0.00", "5.00", "120.00", "2.00",
		"30.00", "11.00", "900.00", "300.00", 4, "100.00", "10.00",
		"300.00", 8, "110.00",
		3, 6, 2, 10, 1, 20,
	)
	previous := externalMetricRow(refreshedAt.Add(-24*time.Hour),
		5, 20, 4,
		"10.00", "50.00", "10.00", "0.00", "5.00", "60.00", "0.00",
		"0.00", "0.00", "400.00", "100.00", 2, "40.00", "0.00",
		"180.00", 5, "100.00",
		1, 5, 1, 8, 0, 10,
	)

	expectExternalMetricQuery(mock, "Asia/Riyadh", "LIKEI", "2026-06-30", "2026-07-01").
		WillReturnRows(sqlmock.NewRows(externalMetricColumns()).AddRow(current...))
	expectExternalMetricQuery(mock, "Asia/Riyadh", "LIKEI", "2026-06-29", "2026-06-30").
		WillReturnRows(sqlmock.NewRows(externalMetricColumns()).AddRow(previous...))
	expectExternalMetricQuery(mock, "Asia/Riyadh", "LIKEI", "2026-06-30", "2026-07-01").
		WillReturnRows(sqlmock.NewRows(externalMetricColumns("period_key")).AddRow(append([]driver.Value{"2026-06-30"}, current...)...))
	expectExternalMetricQuery(mock, "Asia/Riyadh", "LIKEI", "2026-06-30", "2026-07-01").
		WillReturnRows(sqlmock.NewRows(externalMetricColumns("country_code", "country_name")).AddRow(append([]driver.Value{"SA", "Saudi Arabia"}, current...)...))
	expectExternalMetricQuery(mock, "Asia/Riyadh", "LIKEI", "2026-06-30", "2026-07-01").
		WillReturnRows(sqlmock.NewRows(externalMetricColumns("period_key", "country_code", "country_name")).AddRow(append([]driver.Value{"2026-06-30", "SA", "Saudi Arabia"}, current...)...))

	shanghai, err := time.LoadLocation("Asia/Shanghai")
	if err != nil {
		t.Fatalf("load location: %v", err)
	}
	start := time.Date(2026, 6, 30, 0, 0, 0, 0, shanghai)
	overview, err := source.StatisticsOverview(context.Background(), StatisticsQuery{
		AppCode: "yumi",
		StatTZ:  "Asia/Shanghai",
		StartMS: start.UnixMilli(),
		EndMS:   start.AddDate(0, 0, 1).UnixMilli(),
	})
	if err != nil {
		t.Fatalf("StatisticsOverview: %v", err)
	}

	assertEqual(t, overview["app_code"], "yumi")
	assertEqual(t, overview["stat_tz"], "Asia/Riyadh")
	assertEqual(t, overview["request_stat_tz"], "Asia/Shanghai")
	assertInt64(t, overview["recharge_usd_minor"], 12_500)
	assertInt64(t, overview["arppu_usd_minor"], 2_500)
	assertInt64(t, overview["registered_users"], 7)
	assertInt64(t, overview["recharge_users"], 5)
	assertFloat(t, overview["paid_conversion_rate"], 0.125)
	assertFloat(t, overview["recharge_conversion_rate"], 0.125)
	assertFloat(t, overview["payer_rate_delta_pp"], -7.5)
	assertInt64(t, overview["game_turnover"], 300)
	assertInt64(t, overview["game_profit"], 190)
	assertFloat(t, overview["d1_retention_rate"], 0.5)
	if overview["coin_total"] != nil {
		t.Fatalf("coin_total should be nil, got %#v", overview["coin_total"])
	}
	if overview["salary_usd_minor"] != nil {
		t.Fatalf("salary_usd_minor should be nil, got %#v", overview["salary_usd_minor"])
	}

	series, ok := overview["daily_series"].([]map[string]any)
	if !ok || len(series) != 1 {
		t.Fatalf("daily_series mismatch: %#v", overview["daily_series"])
	}
	assertEqual(t, series[0]["stat_day"], "2026-06-30")
	assertInt64(t, series[0]["recharge_usd_minor"], 12_500)

	countries, ok := overview["country_breakdown"].([]map[string]any)
	if !ok || len(countries) != 1 {
		t.Fatalf("country_breakdown mismatch: %#v", overview["country_breakdown"])
	}
	assertEqual(t, countries[0]["country_code"], "SA")
	assertEqual(t, countries[0]["country_name"], "Saudi Arabia")

	sources, ok := overview["report_metric_sources"].([]map[string]any)
	if !ok {
		t.Fatalf("report_metric_sources mismatch: %#v", overview["report_metric_sources"])
	}
	assertMetricSourceUnavailable(t, sources, "coin_total")
	assertMetricSourceUnavailable(t, sources, "salary_usd_minor")
	assertMetricSourceUnavailable(t, sources, "coin_seller_stock_coin")
	assertMetricSourceUnavailable(t, sources, "avg_mic_online_ms")

	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations: %v", err)
	}
}

func TestMySQLExternalDashboardSourceListCoinSellerRechargeBills(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	defer db.Close()

	source := NewMySQLExternalDashboardSource(db, config.DashboardExternalSourceConfig{
		Enabled:        true,
		AppCode:        "Yumi",
		AppName:        "Yumi",
		SysOrigin:      "LIKEI",
		StatTimezone:   "Asia/Riyadh",
		RequestTimeout: time.Second,
	})
	shanghai, err := time.LoadLocation("Asia/Shanghai")
	if err != nil {
		t.Fatalf("load location: %v", err)
	}
	start := time.Date(2026, 6, 7, 0, 0, 0, 0, shanghai)
	end := time.Date(2026, 7, 7, 0, 0, 0, 0, shanghai)

	mock.ExpectQuery("SELECT COUNT\\(\\*\\) FROM \\([\\s\\S]*FROM country_dashboard_period_metric t[\\s\\S]*HAVING COALESCE\\(SUM\\(t\\.dealer_recharge\\), 0\\) <> 0").
		WithArgs("Asia/Riyadh", "LIKEI", "2026-06-07", "2026-07-07").
		WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(int64(2)))
	mock.ExpectQuery("SELECT t\\.period_key, UPPER\\(t\\.country_code\\), MAX\\(t\\.country_name\\), CAST\\(COALESCE\\(SUM\\(t\\.dealer_recharge\\), 0\\) AS CHAR\\)[\\s\\S]*FROM country_dashboard_period_metric t").
		WithArgs("Asia/Riyadh", "LIKEI", "2026-06-07", "2026-07-07", 20, 0).
		WillReturnRows(sqlmock.NewRows([]string{"period_key", "country_code", "country_name", "dealer_recharge"}).
			AddRow("2026-07-05", "SA", "Saudi Arabia", "13686.92").
			AddRow("2026-07-04", "EG", "Egypt", "10.00"))

	items, total, err := source.(CoinSellerRechargeBillSource).ListCoinSellerRechargeBills(context.Background(), CoinSellerRechargeBillQuery{
		StatTZ:   "Asia/Shanghai",
		StartMS:  start.UnixMilli(),
		EndMS:    end.UnixMilli(),
		Page:     1,
		PageSize: 20,
	})
	if err != nil {
		t.Fatalf("ListCoinSellerRechargeBills: %v", err)
	}
	if total != 2 || len(items) != 2 {
		t.Fatalf("unexpected result total=%d items=%+v", total, items)
	}
	if items[0].TransactionID != "yumi-dealer-recharge-2026-07-05-SA" || items[0].Source != "dealer_recharge" || items[0].CountryCode != "SA" {
		t.Fatalf("unexpected first item identity: %+v", items[0])
	}
	if items[0].USDMinorAmount != 1_368_692 {
		t.Fatalf("unexpected first item amount: %+v", items[0])
	}
	expectedCreatedAt := time.Date(2026, 7, 5, 0, 0, 0, 0, shanghai).UnixMilli()
	if items[0].CreatedAtMS != expectedCreatedAt {
		t.Fatalf("unexpected first item created_at: got %d want %d", items[0].CreatedAtMS, expectedCreatedAt)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations: %v", err)
	}
}

func TestExternalDashboardRetentionUsesNilWhenBaseMissing(t *testing.T) {
	metric := externalDashboardMetric{
		D1RetentionUsers:      0,
		D1RetentionBaseUsers:  0,
		D7RetentionUsers:      0,
		D7RetentionBaseUsers:  5,
		D30RetentionUsers:     0,
		D30RetentionBaseUsers: 0,
	}
	row := metric.toMap()
	if row["d1_retention_rate"] != nil || row["d1_retention_users"] != nil || row["d1_retention_base_users"] != nil {
		t.Fatalf("d1 retention without base should be nil, got %#v", row)
	}
	if row["d7_retention_rate"] != float64(0) || row["d7_retention_users"] != int64(0) || row["d7_retention_base_users"] != int64(5) {
		t.Fatalf("d7 observed zero retention should stay visible, got %#v", row)
	}
	retention := metric.retentionMap()
	if retention["day30_rate"] != nil || retention["day30_users"] != nil || retention["day30_base_users"] != nil {
		t.Fatalf("d30 retention without base should be nil, got %#v", retention)
	}
}

func TestMySQLExternalDashboardSourceFiltersLegacyRegionCountries(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	defer db.Close()

	source := NewMySQLExternalDashboardSource(db, config.DashboardExternalSourceConfig{
		Enabled:        true,
		AppCode:        "Yumi",
		AppName:        "Yumi",
		SysOrigin:      "LIKEI",
		StatTimezone:   "Asia/Riyadh",
		RequestTimeout: time.Second,
	}, staticDashboardRegionResolver{appCode: "yumi", regionID: 1001, countryCodes: []string{"SA", "AE"}})
	if source == nil {
		t.Fatal("source is nil")
	}

	refreshedAt := time.Date(2026, 6, 30, 23, 0, 0, 0, time.UTC)
	current := externalMetricRow(refreshedAt,
		7, 40, 5,
		"25.00", "100.00", "20.00", "0.00", "5.00", "120.00", "2.00",
		"30.00", "11.00", "900.00", "300.00", 4, "100.00", "10.00",
		"300.00", 8, "110.00",
		3, 6, 2, 10, 1, 20,
	)
	previous := externalMetricRow(refreshedAt.Add(-24*time.Hour),
		5, 20, 4,
		"10.00", "50.00", "10.00", "0.00", "5.00", "60.00", "0.00",
		"0.00", "0.00", "400.00", "100.00", 2, "40.00", "0.00",
		"180.00", 5, "100.00",
		1, 5, 1, 8, 0, 10,
	)

	expectExternalMetricQueryWithCountries(mock, "Asia/Riyadh", "LIKEI", "2026-06-30", "2026-07-01", "AE", "SA").
		WillReturnRows(sqlmock.NewRows(externalMetricColumns()).AddRow(current...))
	expectExternalMetricQueryWithCountries(mock, "Asia/Riyadh", "LIKEI", "2026-06-29", "2026-06-30", "AE", "SA").
		WillReturnRows(sqlmock.NewRows(externalMetricColumns()).AddRow(previous...))
	expectExternalMetricQueryWithCountries(mock, "Asia/Riyadh", "LIKEI", "2026-06-30", "2026-07-01", "AE", "SA").
		WillReturnRows(sqlmock.NewRows(externalMetricColumns("period_key")).AddRow(append([]driver.Value{"2026-06-30"}, current...)...))
	expectExternalMetricQueryWithCountries(mock, "Asia/Riyadh", "LIKEI", "2026-06-30", "2026-07-01", "AE", "SA").
		WillReturnRows(sqlmock.NewRows(externalMetricColumns("country_code", "country_name")).AddRow(append([]driver.Value{"SA", "Saudi Arabia"}, current...)...))
	expectExternalMetricQueryWithCountries(mock, "Asia/Riyadh", "LIKEI", "2026-06-30", "2026-07-01", "AE", "SA").
		WillReturnRows(sqlmock.NewRows(externalMetricColumns("period_key", "country_code", "country_name")).AddRow(append([]driver.Value{"2026-06-30", "SA", "Saudi Arabia"}, current...)...))

	shanghai, err := time.LoadLocation("Asia/Shanghai")
	if err != nil {
		t.Fatalf("load location: %v", err)
	}
	start := time.Date(2026, 6, 30, 0, 0, 0, 0, shanghai)
	overview, err := source.StatisticsOverview(context.Background(), StatisticsQuery{
		AppCode:  "yumi",
		StatTZ:   "Asia/Shanghai",
		StartMS:  start.UnixMilli(),
		EndMS:    start.AddDate(0, 0, 1).UnixMilli(),
		RegionID: 1001,
	})
	if err != nil {
		t.Fatalf("StatisticsOverview: %v", err)
	}
	countries, ok := overview["country_breakdown"].([]map[string]any)
	if !ok || len(countries) != 1 {
		t.Fatalf("country_breakdown mismatch: %#v", overview["country_breakdown"])
	}
	assertEqual(t, countries[0]["region_id"], int64(1001))
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations: %v", err)
	}
}

func expectExternalMetricQuery(mock sqlmock.Sqlmock, statTimezone string, sysOrigin string, startDate string, endDate string) *sqlmock.ExpectedQuery {
	return mock.ExpectQuery("FROM country_dashboard_period_metric").
		WithArgs(statTimezone, sysOrigin, startDate, endDate)
}

func expectExternalMetricQueryWithCountries(mock sqlmock.Sqlmock, statTimezone string, sysOrigin string, startDate string, endDate string, countryCodes ...string) *sqlmock.ExpectedQuery {
	args := []driver.Value{statTimezone, sysOrigin, startDate, endDate}
	for _, countryCode := range countryCodes {
		args = append(args, countryCode)
	}
	return mock.ExpectQuery("FROM country_dashboard_period_metric").
		WithArgs(args...)
}

type staticDashboardRegionResolver struct {
	appCode      string
	regionID     int64
	countryCodes []string
}

func (r staticDashboardRegionResolver) CountryCodesForRegion(_ context.Context, appCode string, regionID int64) ([]string, bool, error) {
	if r.appCode != appCode || r.regionID != regionID {
		return nil, false, nil
	}
	return r.countryCodes, true, nil
}

func externalMetricColumns(prefix ...string) []string {
	columns := []string{
		"country_new_user",
		"daily_active_user",
		"daily_recharge_user",
		"new_user_recharge",
		"official_recharge",
		"mifapay_recharge",
		"google_recharge",
		"dealer_recharge",
		"user_recharge",
		"new_dealer_user_recharge",
		"salary_exchange",
		"salary_transfer",
		"gift_consume",
		"lucky_gift_total_flow",
		"lucky_gift_user",
		"lucky_gift_payout",
		"lucky_gift_anchor_share",
		"game_total_flow",
		"game_user",
		"game_payout",
		"d1_retention_user",
		"d1_retention_base_user",
		"d7_retention_user",
		"d7_retention_base_user",
		"d30_retention_user",
		"d30_retention_base_user",
		"refreshed_at",
	}
	return append(prefix, columns...)
}

func externalMetricRow(refreshedAt time.Time,
	newUsers int64, activeUsers int64, paidUsers int64,
	newUserRecharge string, officialRecharge string, mifaPayRecharge string, googleRecharge string, dealerRecharge string, userRecharge string, newDealerRecharge string,
	salaryExchange string, salaryTransfer string, giftConsume string, luckyGiftTurnover string, luckyGiftPayers int64, luckyGiftPayout string, luckyGiftAnchorShare string,
	gameTurnover string, gamePlayers int64, gamePayout string,
	d1Users int64, d1Base int64, d7Users int64, d7Base int64, d30Users int64, d30Base int64,
) []driver.Value {
	return []driver.Value{
		newUsers,
		activeUsers,
		paidUsers,
		newUserRecharge,
		officialRecharge,
		mifaPayRecharge,
		googleRecharge,
		dealerRecharge,
		userRecharge,
		newDealerRecharge,
		salaryExchange,
		salaryTransfer,
		giftConsume,
		luckyGiftTurnover,
		luckyGiftPayers,
		luckyGiftPayout,
		luckyGiftAnchorShare,
		gameTurnover,
		gamePlayers,
		gamePayout,
		d1Users,
		d1Base,
		d7Users,
		d7Base,
		d30Users,
		d30Base,
		refreshedAt,
	}
}

func assertEqual(t *testing.T, got any, want any) {
	t.Helper()
	if got != want {
		t.Fatalf("got %#v, want %#v", got, want)
	}
}

func assertInt64(t *testing.T, got any, want int64) {
	t.Helper()
	value, ok := got.(int64)
	if !ok {
		t.Fatalf("got %T(%#v), want int64(%d)", got, got, want)
	}
	if value != want {
		t.Fatalf("got %d, want %d", value, want)
	}
}

func assertFloat(t *testing.T, got any, want float64) {
	t.Helper()
	value, ok := got.(float64)
	if !ok {
		t.Fatalf("got %T(%#v), want float64(%f)", got, got, want)
	}
	if math.Abs(value-want) > 0.000001 {
		t.Fatalf("got %f, want %f", value, want)
	}
}

func assertMetricSourceUnavailable(t *testing.T, sources []map[string]any, field string) {
	t.Helper()
	for _, source := range sources {
		if source["field"] != field {
			continue
		}
		if source["available"] != false {
			t.Fatalf("%s should be unavailable: %#v", field, source)
		}
		if source["missing_reason"] == "" {
			t.Fatalf("%s missing reason is empty: %#v", field, source)
		}
		return
	}
	t.Fatalf("metric source %s not found: %#v", field, sources)
}
