package app

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"runtime"
	"strings"
	"testing"
	"time"

	"hyapp/internal/testutil/mysqlschema"
	"hyapp/pkg/activitymq"
	mysqlstorage "hyapp/services/statistics-service/internal/storage/mysql"
)

func TestStatisticsActivityTagExpressionOnlyIncludesModeledFacts(t *testing.T) {
	expression, err := activitymq.LegacyCompatibleTagExpression(statisticsActivityEventTypes()...)
	if err != nil {
		t.Fatalf("activity tag expression: %v", err)
	}
	for _, tag := range []string{
		activitymq.TagActivityTemplateFact,
		activitymq.EventTypeGiftScored,
		activitymq.EventTypeTaskProgressed,
		activitymq.EventTypeTaskClaimed,
		activitymq.EventTypeActivityVisited,
	} {
		if !strings.Contains(expression, tag) {
			t.Fatalf("activity selector %q misses %q", expression, tag)
		}
	}
}

func TestActivityTemplateDataHTTPRejectsPartialUTCDay(t *testing.T) {
	start := time.Date(2026, time.July, 14, 1, 0, 0, 0, time.UTC).UnixMilli()
	request := httptest.NewRequest(http.MethodGet, fmt.Sprintf(
		"/internal/v1/statistics/activity-templates/data?app_code=lalu&template_id=template-1&published_version=2&start_ms=%d&end_ms=%d",
		start, start+24*time.Hour.Milliseconds(),
	), nil)
	response := httptest.NewRecorder()
	(&queryHTTPServer{}).activityTemplateData(response, request)
	if response.Code != http.StatusBadRequest || !strings.Contains(response.Body.String(), "UTC 00:00") {
		t.Fatalf("unexpected partial-day response: status=%d body=%s", response.Code, response.Body.String())
	}
}

func TestSocialRequirementsHTTPRejectsBaseRangeOver31Days(t *testing.T) {
	start := time.Date(2026, time.July, 1, 0, 0, 0, 0, time.UTC)
	request := httptest.NewRequest(http.MethodGet, fmt.Sprintf(
		"/internal/v1/statistics/social/requirements?app_code=lalu&section=retention&start_ms=%d&end_ms=%d",
		start.UnixMilli(), start.AddDate(0, 0, 32).UnixMilli(),
	), nil)
	response := httptest.NewRecorder()
	// nil repo proves the range guard rejects the request before any database query.
	(&queryHTTPServer{}).socialRequirements(response, request)
	if response.Code != http.StatusBadRequest || !strings.Contains(response.Body.String(), "at most 31 days") {
		t.Fatalf("unexpected over-range response: status=%d body=%s", response.Code, response.Body.String())
	}
}

func TestActivityTemplateMQProjectionAndQueryHTTPAreIdempotent(t *testing.T) {
	ctx := context.Background()
	_, file, _, _ := runtime.Caller(0)
	schema := mysqlschema.New(t, mysqlschema.Config{
		InitDBPath:     mysqlschema.InitDBPath(t, file, "../../deploy/mysql/initdb/001_statistics_service.sql"),
		DatabasePrefix: "hy_stats_activity_template",
	})
	repo, err := mysqlstorage.Open(ctx, schema.DSN)
	if err != nil {
		t.Fatalf("open statistics repository: %v", err)
	}
	t.Cleanup(func() { _ = repo.Close() })

	dayStart := time.Date(2026, time.July, 14, 0, 0, 0, 0, time.UTC)
	base := activitymq.ActivityTemplateFactMessage{
		AppCode: "lalu", TemplateID: "template-1", TemplateCode: "summer_rank", PublishedVersion: 2,
		StatDay: "2026-07-14", RegionID: 210, OccurredAtMS: dayStart.Add(8 * time.Hour).UnixMilli(),
	}
	facts := []activitymq.ActivityTemplateFactMessage{
		mergeActivityFact(base, activitymq.ActivityTemplateFactMessage{
			EventID: "activity:visit:1", EventType: activitymq.EventTypeActivityVisited, UserID: 42,
			VisitCountDelta: 1,
		}),
		mergeActivityFact(base, activitymq.ActivityTemplateFactMessage{
			EventID: "activity:gift:1", EventType: activitymq.EventTypeGiftScored, UserID: 42,
			GiftID: "rose", GiftCountDelta: 2, CoinAmountDelta: 200,
		}),
		mergeActivityFact(base, activitymq.ActivityTemplateFactMessage{
			EventID: "activity:gift:2", EventType: activitymq.EventTypeGiftScored, UserID: 43,
			RegionID: 211, GiftID: "rose", GiftCountDelta: 1, CoinAmountDelta: 100,
		}),
		mergeActivityFact(base, activitymq.ActivityTemplateFactMessage{
			EventID: "activity:task:1", EventType: activitymq.EventTypeTaskProgressed, UserID: 42,
			TaskKey: "send_gift", TaskType: "gift", ProgressDelta: 5, CompletedDelta: 1,
		}),
		mergeActivityFact(base, activitymq.ActivityTemplateFactMessage{
			EventID: "activity:task:2", EventType: activitymq.EventTypeTaskProgressed, UserID: 43,
			RegionID: 211, TaskKey: "send_gift", TaskType: "gift", ProgressDelta: 2,
		}),
		mergeActivityFact(base, activitymq.ActivityTemplateFactMessage{
			EventID: "activity:claim:1", EventType: activitymq.EventTypeTaskClaimed, UserID: 42,
			TaskKey: "send_gift", TaskType: "gift", ClaimedDelta: 1,
		}),
	}
	for _, fact := range facts {
		body, encodeErr := activitymq.EncodeActivityTemplateFactMessage(fact)
		if encodeErr != nil {
			t.Fatalf("encode %s: %v", fact.EventID, encodeErr)
		}
		if err := ConsumeActivityOutboxBody(ctx, repo, body); err != nil {
			t.Fatalf("consume %s: %v", fact.EventID, err)
		}
		// RocketMQ is at-least-once. Replaying the same event must leave all
		// counters and participant projections unchanged.
		if fact.EventID == "activity:gift:1" {
			if err := ConsumeActivityOutboxBody(ctx, repo, body); err != nil {
				t.Fatalf("consume duplicate %s: %v", fact.EventID, err)
			}
		}
	}

	server, err := newQueryHTTPServer("127.0.0.1:0", repo)
	if err != nil {
		t.Fatalf("create query server: %v", err)
	}
	t.Cleanup(func() {
		closeCtx, cancel := context.WithTimeout(context.Background(), time.Second)
		defer cancel()
		_ = server.Close(closeCtx)
	})
	go func() { _ = server.Run() }()

	url := fmt.Sprintf(
		"http://%s/internal/v1/statistics/activity-templates/data?app_code=lalu&template_id=template-1&published_version=2&start_ms=%d&end_ms=%d",
		server.listener.Addr().String(), dayStart.UnixMilli(), dayStart.Add(24*time.Hour).UnixMilli(),
	)
	response, err := http.Get(url)
	if err != nil {
		t.Fatalf("query activity data: %v", err)
	}
	defer response.Body.Close()
	if response.StatusCode != http.StatusOK {
		t.Fatalf("unexpected status: %s", response.Status)
	}
	var data mysqlstorage.ActivityTemplateData
	if err := json.NewDecoder(response.Body).Decode(&data); err != nil {
		t.Fatalf("decode activity data: %v", err)
	}
	if data.Overview.Participants != 2 || data.Overview.VisitCount != 1 || data.Overview.GiftUsers != 2 || data.Overview.TaskUsers != 2 ||
		data.Overview.GiftEventCount != 2 || data.Overview.GiftCount != 3 || data.Overview.CoinAmount != 300 ||
		data.Overview.TaskProgress != 7 || data.Overview.TaskCompleted != 1 || data.Overview.TaskClaimed != 1 {
		t.Fatalf("unexpected overview: %+v", data.Overview)
	}
	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("unexpected query semantics: %+v", data)
	}
	if len(data.Trend) != 1 || data.Trend[0].Participants != 2 || data.Trend[0].VisitCount != 1 || data.Trend[0].GiftUsers != 2 || data.Trend[0].TaskUsers != 2 {
		t.Fatalf("unexpected trend: %+v", data.Trend)
	}
	if len(data.Gifts) != 1 || data.Gifts[0].GiftID != "rose" || data.Gifts[0].GiftUsers != 2 || data.Gifts[0].GiftCount != 3 {
		t.Fatalf("unexpected gifts: %+v", data.Gifts)
	}
	if len(data.Tasks) != 1 || data.Tasks[0].TaskKey != "send_gift" || data.Tasks[0].TaskUsers != 2 || data.Tasks[0].TaskClaimed != 1 {
		t.Fatalf("unexpected tasks: %+v", data.Tasks)
	}

	regionResponse, err := http.Get(url + "&region_id=210")
	if err != nil {
		t.Fatalf("query regional activity data: %v", err)
	}
	defer regionResponse.Body.Close()
	if regionResponse.StatusCode != http.StatusOK {
		t.Fatalf("unexpected regional status: %s", regionResponse.Status)
	}
	var regionData mysqlstorage.ActivityTemplateData
	if err := json.NewDecoder(regionResponse.Body).Decode(&regionData); err != nil {
		t.Fatalf("decode regional activity data: %v", err)
	}
	if regionData.RegionID != 210 || regionData.Overview.Participants != 1 || regionData.Overview.VisitCount != 1 || regionData.Overview.GiftUsers != 1 ||
		regionData.Overview.TaskUsers != 1 || regionData.Overview.GiftCount != 2 || regionData.Overview.CoinAmount != 200 ||
		regionData.Overview.TaskProgress != 5 || regionData.Overview.TaskCompleted != 1 || regionData.Overview.TaskClaimed != 1 {
		t.Fatalf("unexpected regional overview: %+v", regionData.Overview)
	}
}

func mergeActivityFact(base, override activitymq.ActivityTemplateFactMessage) activitymq.ActivityTemplateFactMessage {
	if override.EventID != "" {
		base.EventID = override.EventID
	}
	if override.EventType != "" {
		base.EventType = override.EventType
	}
	if override.RegionID != 0 {
		base.RegionID = override.RegionID
	}
	base.UserID = override.UserID
	base.GiftID = override.GiftID
	base.TaskKey = override.TaskKey
	base.TaskType = override.TaskType
	base.GiftCountDelta = override.GiftCountDelta
	base.CoinAmountDelta = override.CoinAmountDelta
	base.ProgressDelta = override.ProgressDelta
	base.CompletedDelta = override.CompletedDelta
	base.ClaimedDelta = override.ClaimedDelta
	base.VisitCountDelta = override.VisitCountDelta
	if override.AttributionAtMS > 0 {
		base.AttributionAtMS = override.AttributionAtMS
	}
	return base
}
