package mysql

import (
	"context"
	"os"
	"path/filepath"
	"regexp"
	"runtime"
	"strings"
	"testing"
	"time"

	"github.com/DATA-DOG/go-sqlmock"
	"hyapp/pkg/xerr"
)

func TestActivityTemplateSchemaIsShippedForFreshAndExistingDatabases(t *testing.T) {
	_, file, _, _ := runtime.Caller(0)
	serviceRoot := filepath.Clean(filepath.Join(filepath.Dir(file), "..", "..", ".."))
	sources := make(map[string]string, 3)
	for _, relativePath := range []string{"deploy/mysql/initdb/001_statistics_service.sql", "deploy/mysql/migrations/002_activity_template_stats.sql"} {
		content, err := os.ReadFile(filepath.Join(serviceRoot, relativePath))
		if err != nil {
			t.Fatalf("read %s: %v", relativePath, err)
		}
		sources[relativePath] = string(content)
	}
	sources["runtime migration"] = strings.Join(activityTemplateStatSchemaStatements(), ";\n") + ";"
	for _, table := range []string{
		"stat_activity_template_summary",
		"stat_activity_template_day",
		"stat_activity_template_gift_day",
		"stat_activity_template_task_day",
		"stat_activity_template_participants",
	} {
		want := normalizedActivityTableDDL(t, sources["deploy/mysql/initdb/001_statistics_service.sql"], table)
		for _, source := range []string{"deploy/mysql/migrations/002_activity_template_stats.sql", "runtime migration"} {
			if got := normalizedActivityTableDDL(t, sources[source], table); got != want {
				t.Fatalf("%s schema drift for %s\nwant: %s\n got: %s", source, table, want, got)
			}
		}
	}
}

func normalizedActivityTableDDL(t *testing.T, ddl, table string) string {
	t.Helper()
	startMarker := "CREATE TABLE IF NOT EXISTS " + table
	start := strings.Index(ddl, startMarker)
	if start < 0 {
		t.Fatalf("DDL misses %s", table)
	}
	endOffset := strings.Index(ddl[start:], ";")
	if endOffset < 0 {
		t.Fatalf("DDL for %s is not terminated", table)
	}
	statement := ddl[start : start+endOffset]
	// initdb/migration carry operator-facing table comments while the runtime
	// CREATE IF NOT EXISTS intentionally does not; comments are not schema.
	statement = regexp.MustCompile(` COMMENT='[^']*'`).ReplaceAllString(statement, "")
	return strings.Join(strings.Fields(statement), " ")
}

func TestActivityTemplateGiftUpsertAddsUniqueParticipantExactlyOnce(t *testing.T) {
	const increment = "participant_count = participant_count + VALUES(participant_count)"
	if got := strings.Count(activityTemplateGiftDayUpsert, increment); got != 1 {
		t.Fatalf("gift aggregate participant increment appears %d times; want exactly one", got)
	}
}

func TestEnsureActivityTemplateColumnIsIdempotent(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("create sql mock: %v", err)
	}
	t.Cleanup(func() { _ = db.Close() })
	repo := &Repository{db: db}
	columnQuery := `SELECT COUNT\(\*\) FROM information_schema\.COLUMNS WHERE TABLE_SCHEMA = DATABASE\(\) AND TABLE_NAME = \? AND COLUMN_NAME = \?`

	mock.ExpectQuery(columnQuery).
		WithArgs("stat_activity_template_day", "visit_event_count").
		WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1))
	if err := repo.ensureActivityTemplateColumn(context.Background(), "stat_activity_template_day", "visit_event_count", "visit_event_count BIGINT NOT NULL DEFAULT 0"); err != nil {
		t.Fatalf("existing column must be a no-op: %v", err)
	}

	mock.ExpectQuery(columnQuery).
		WithArgs("stat_activity_template_day", "visit_event_count").
		WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(0))
	mock.ExpectExec(`ALTER TABLE stat_activity_template_day ADD COLUMN visit_event_count BIGINT NOT NULL DEFAULT 0`).
		WillReturnResult(sqlmock.NewResult(0, 0))
	if err := repo.ensureActivityTemplateColumn(context.Background(), "stat_activity_template_day", "visit_event_count", "visit_event_count BIGINT NOT NULL DEFAULT 0"); err != nil {
		t.Fatalf("missing column must be added: %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("unmet SQL expectations: %v", err)
	}
}

func TestQueryActivityTemplateDataEmptyResultKeepsStableZeroShape(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("create sql mock: %v", err)
	}
	t.Cleanup(func() { _ = db.Close() })
	repo := &Repository{db: db}
	start := time.Date(2026, time.July, 14, 0, 0, 0, 0, time.UTC).UnixMilli()
	end := start + 24*time.Hour.Milliseconds()

	mock.ExpectQuery(`SELECT COALESCE\(MAX\(template_code\), ''\)`).
		WithArgs("lalu", "template-1", int64(2), "2026-07-14", "2026-07-14", int64(0), int64(0)).
		WillReturnRows(sqlmock.NewRows([]string{
			"template_code", "visit_event_count", "gift_event_count", "gift_count", "coin_amount", "progress_value", "completed_count", "claimed_count",
		}).AddRow("", int64(0), int64(0), int64(0), int64(0), int64(0), int64(0), int64(0)))
	mock.ExpectQuery(`SELECT COUNT\(DISTINCT CASE WHEN scope_type = \? THEN user_id END\)`).
		WithArgs(
			activityParticipantScopeDay, activityParticipantScopeGift, activityParticipantScopeTask,
			"lalu", "template-1", int64(2),
			activityParticipantScopeDay, activityParticipantScopeGift, activityParticipantScopeTask,
			int64(0), "2026-07-14", "2026-07-14",
		).
		WillReturnRows(sqlmock.NewRows([]string{"participants", "gift_users", "task_users"}).AddRow(int64(0), int64(0), int64(0)))
	mock.ExpectQuery(`SELECT d\.stat_day`).
		WithArgs(activityParticipantScopeDay, int64(0), activityParticipantScopeGift, int64(0), activityParticipantScopeTask, int64(0),
			"lalu", "template-1", int64(2), "2026-07-14", "2026-07-14", int64(0), int64(0)).
		WillReturnRows(sqlmock.NewRows([]string{"stat_day"}))
	mock.ExpectQuery(`SELECT g\.gift_id`).
		WithArgs(activityParticipantScopeGift, int64(0), "2026-07-14", "2026-07-14", "lalu", "template-1", int64(2),
			"2026-07-14", "2026-07-14", int64(0), int64(0), activityDimensionLimit).
		WillReturnRows(sqlmock.NewRows([]string{"gift_id"}))
	mock.ExpectQuery(`SELECT t\.task_key`).
		WithArgs(activityParticipantScopeTask, int64(0), "2026-07-14", "2026-07-14", "lalu", "template-1", int64(2),
			"2026-07-14", "2026-07-14", int64(0), int64(0), activityDimensionLimit).
		WillReturnRows(sqlmock.NewRows([]string{"task_key"}))

	data, err := repo.QueryActivityTemplateData(context.Background(), ActivityTemplateDataQuery{
		AppCode: "lalu", TemplateID: "template-1", PublishedVersion: 2, StartMS: start, EndMS: end,
	})
	if err != nil {
		t.Fatalf("query empty activity data: %v", err)
	}
	if data.Overview.TemplateID != "template-1" || data.Overview.PublishedVersion != 2 || data.Overview.Participants != 0 ||
		data.Overview.GiftUsers != 0 || data.Overview.TaskUsers != 0 || data.Overview.GiftCount != 0 || data.Overview.TaskCompleted != 0 {
		t.Fatalf("unexpected empty overview: %+v", data.Overview)
	}
	if data.Trend == nil || data.Gifts == nil || data.Tasks == nil || len(data.Trend) != 0 || len(data.Gifts) != 0 || len(data.Tasks) != 0 {
		t.Fatalf("empty collections must be non-null empty arrays: %+v", data)
	}
	if data.Granularity != "day" || data.Timezone != "UTC" || data.DimensionLimit != 100 ||
		data.GiftSort != "coin_amount_desc,gift_count_desc,gift_id_asc" ||
		data.TaskSort != "task_completed_desc,task_claimed_desc,task_key_asc" {
		t.Fatalf("query semantics missing from empty result: %+v", data)
	}

	// Region queries must switch to the regional uniqueness scopes rather than
	// summing all-region users, because one user may have touched multiple regions.
	mock.ExpectQuery(`SELECT COALESCE\(MAX\(template_code\), ''\)`).
		WithArgs("lalu", "template-1", int64(2), "2026-07-14", "2026-07-14", int64(210), int64(210)).
		WillReturnRows(sqlmock.NewRows([]string{
			"template_code", "visit_event_count", "gift_event_count", "gift_count", "coin_amount", "progress_value", "completed_count", "claimed_count",
		}).AddRow("", int64(0), int64(0), int64(0), int64(0), int64(0), int64(0), int64(0)))
	mock.ExpectQuery(`SELECT COUNT\(DISTINCT CASE WHEN scope_type = \? THEN user_id END\)`).
		WithArgs(
			activityParticipantScopeDayRegion, activityParticipantScopeGiftRegion, activityParticipantScopeTaskRegion,
			"lalu", "template-1", int64(2),
			activityParticipantScopeDayRegion, activityParticipantScopeGiftRegion, activityParticipantScopeTaskRegion,
			int64(210), "2026-07-14", "2026-07-14",
		).
		WillReturnRows(sqlmock.NewRows([]string{"participants", "gift_users", "task_users"}).AddRow(int64(0), int64(0), int64(0)))
	mock.ExpectQuery(`SELECT d\.stat_day`).
		WithArgs(activityParticipantScopeDayRegion, int64(210), activityParticipantScopeGiftRegion, int64(210), activityParticipantScopeTaskRegion, int64(210),
			"lalu", "template-1", int64(2), "2026-07-14", "2026-07-14", int64(210), int64(210)).
		WillReturnRows(sqlmock.NewRows([]string{"stat_day"}))
	mock.ExpectQuery(`SELECT g\.gift_id`).
		WithArgs(activityParticipantScopeGiftRegion, int64(210), "2026-07-14", "2026-07-14", "lalu", "template-1", int64(2),
			"2026-07-14", "2026-07-14", int64(210), int64(210), activityDimensionLimit).
		WillReturnRows(sqlmock.NewRows([]string{"gift_id"}))
	mock.ExpectQuery(`SELECT t\.task_key`).
		WithArgs(activityParticipantScopeTaskRegion, int64(210), "2026-07-14", "2026-07-14", "lalu", "template-1", int64(2),
			"2026-07-14", "2026-07-14", int64(210), int64(210), activityDimensionLimit).
		WillReturnRows(sqlmock.NewRows([]string{"task_key"}))
	regionData, err := repo.QueryActivityTemplateData(context.Background(), ActivityTemplateDataQuery{
		AppCode: "lalu", TemplateID: "template-1", PublishedVersion: 2, RegionID: 210, StartMS: start, EndMS: end,
	})
	if err != nil {
		t.Fatalf("query empty regional activity data: %v", err)
	}
	if regionData.RegionID != 210 || regionData.Trend == nil || regionData.Gifts == nil || regionData.Tasks == nil {
		t.Fatalf("unexpected regional empty shape: %+v", regionData)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("unmet SQL expectations: %v", err)
	}
}

func TestQueryActivityTemplateDataRejectsPartialUTCDay(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("create sql mock: %v", err)
	}
	t.Cleanup(func() { _ = db.Close() })
	start := time.Date(2026, time.July, 14, 1, 0, 0, 0, time.UTC).UnixMilli()
	_, err = (&Repository{db: db}).QueryActivityTemplateData(context.Background(), ActivityTemplateDataQuery{
		AppCode: "lalu", TemplateID: "template-1", PublishedVersion: 2,
		StartMS: start, EndMS: start + 24*time.Hour.Milliseconds(),
	})
	if !xerr.IsCode(err, xerr.InvalidArgument) || !strings.Contains(err.Error(), "UTC 00:00") {
		t.Fatalf("expected UTC day alignment error, got %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("unaligned range must not query aggregate tables: %v", err)
	}
}
