package mictime_test

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

	mictimedomain "hyapp/services/user-service/internal/domain/mictime"
	"hyapp/services/user-service/internal/testutil/mysqltest"
)

func TestProcessRoomMicEventsAggregatesLifetimeAndDeduplicates(t *testing.T) {
	wrapped := mysqltest.NewRepository(t)
	repo := wrapped.Repository.MicTimeRepository()
	ctx := context.Background()
	base := time.Date(2026, 5, 8, 10, 0, 0, 0, time.UTC).UnixMilli()
	micSessionID := "mic_session_normal"

	mustApply(t, repo, roomMicEvent("evt-up", micSessionID, mictimedomain.ActionUp, 10001, 0, 1, base))
	mustApply(t, repo, roomMicEvent("evt-confirm", micSessionID, mictimedomain.ActionPublishConfirmed, 10001, 1, 1, base+60_000))
	mustApply(t, repo, roomMicEvent("evt-change", micSessionID, mictimedomain.ActionChange, 10001, 1, 2, base+5*60_000))
	mustApply(t, repo, roomMicEvent("evt-down", micSessionID, mictimedomain.ActionDown, 10001, 2, 0, base+11*60_000))

	stats, err := repo.GetLifetimeStats(ctx, "lalu", 10001)
	if err != nil {
		t.Fatalf("GetLifetimeStats failed: %v", err)
	}
	if stats.SeatOccupiedMs != 11*60_000 || stats.MicOnlineMs != 10*60_000 || stats.SessionCount != 1 {
		t.Fatalf("normal mic duration mismatch: %+v", stats)
	}
	assertUserOutboxEvent(t, wrapped.RawDB(), "UserMicSessionClosed:"+micSessionID)
	assertUserOutboxEvent(t, wrapped.RawDB(), "UserMicLifetimeStatsUpdated:"+micSessionID)
	assertUserOutboxEvent(t, wrapped.RawDB(), "UserMicDailyStatsUpdated:"+micSessionID+":2026-05-08")

	duplicate, err := repo.ProcessRoomMicEvent(ctx, roomMicEvent("evt-down", micSessionID, mictimedomain.ActionDown, 10001, 2, 0, base+11*60_000))
	if err != nil || !duplicate.Skipped || duplicate.Reason != "already_consumed" {
		t.Fatalf("duplicate down should skip: result=%+v err=%v", duplicate, err)
	}
	stats, _ = repo.GetLifetimeStats(ctx, "lalu", 10001)
	if stats.SeatOccupiedMs != 11*60_000 || stats.MicOnlineMs != 10*60_000 || stats.SessionCount != 1 {
		t.Fatalf("duplicate down must not double count: %+v", stats)
	}
}

func TestProcessPendingPublishTimeoutCountsSeatButNotMicOnline(t *testing.T) {
	repo := mysqltest.NewRepository(t).Repository.MicTimeRepository()
	ctx := context.Background()
	base := time.Date(2026, 5, 8, 12, 0, 0, 0, time.UTC).UnixMilli()
	micSessionID := "mic_session_pending_timeout"

	mustApply(t, repo, roomMicEvent("evt-pending-up", micSessionID, mictimedomain.ActionUp, 10002, 0, 1, base))
	down := roomMicEvent("evt-pending-down", micSessionID, mictimedomain.ActionDown, 10002, 1, 0, base+2*60_000)
	down.Reason = "publish_timeout"
	mustApply(t, repo, down)

	stats, err := repo.GetLifetimeStats(ctx, "lalu", 10002)
	if err != nil {
		t.Fatalf("GetLifetimeStats failed: %v", err)
	}
	if stats.SeatOccupiedMs != 2*60_000 || stats.MicOnlineMs != 0 || stats.SessionCount != 1 {
		t.Fatalf("pending timeout duration mismatch: %+v", stats)
	}
}

func TestCompensateOpenSessionsClosesPendingAndPublishingSessions(t *testing.T) {
	wrapped := mysqltest.NewRepository(t)
	repo := wrapped.Repository.MicTimeRepository()
	ctx := context.Background()
	base := time.Date(2026, 5, 8, 14, 0, 0, 0, time.UTC).UnixMilli()

	pendingSessionID := "mic_session_comp_pending"
	mustApply(t, repo, roomMicEvent("evt-comp-pending-up", pendingSessionID, mictimedomain.ActionUp, 10004, 0, 1, base))

	publishingSessionID := "mic_session_comp_publishing"
	mustApply(t, repo, roomMicEvent("evt-comp-pub-up", publishingSessionID, mictimedomain.ActionUp, 10005, 0, 1, base))
	mustApply(t, repo, roomMicEvent("evt-comp-pub-confirm", publishingSessionID, mictimedomain.ActionPublishConfirmed, 10005, 1, 1, base+60_000))

	result, err := repo.CompensateOpenSessions(ctx, mictimedomain.CompensationOptions{
		NowMs:                     base + 3*60*60*1000,
		BatchSize:                 10,
		PendingPublishMaxAgeMs:    2 * 60 * 1000,
		PublishingSessionMaxAgeMs: 60 * 60 * 1000,
	})
	if err != nil {
		t.Fatalf("CompensateOpenSessions failed: %v", err)
	}
	if result.ClosedCount != 2 || result.PendingPublishClosed != 1 || result.PublishingSessionClosed != 1 {
		t.Fatalf("compensation result mismatch: %+v", result)
	}

	pendingStats, err := repo.GetLifetimeStats(ctx, "lalu", 10004)
	if err != nil {
		t.Fatalf("GetLifetimeStats pending failed: %v", err)
	}
	if pendingStats.SeatOccupiedMs != 2*60*1000 || pendingStats.MicOnlineMs != 0 || pendingStats.SessionCount != 1 {
		t.Fatalf("pending compensation stats mismatch: %+v", pendingStats)
	}
	assertSessionEnded(t, wrapped.RawDB(), pendingSessionID, mictimedomain.CompensationReasonPendingPublishTimeout)
	assertUserOutboxEvent(t, wrapped.RawDB(), "UserMicSessionClosed:"+pendingSessionID)

	publishingStats, err := repo.GetLifetimeStats(ctx, "lalu", 10005)
	if err != nil {
		t.Fatalf("GetLifetimeStats publishing failed: %v", err)
	}
	if publishingStats.SeatOccupiedMs != 60*60*1000 || publishingStats.MicOnlineMs != 59*60*1000 || publishingStats.SessionCount != 1 {
		t.Fatalf("publishing compensation stats mismatch: %+v", publishingStats)
	}
	assertSessionEnded(t, wrapped.RawDB(), publishingSessionID, mictimedomain.CompensationReasonPublishingMaxWindow)
	assertUserOutboxEvent(t, wrapped.RawDB(), "UserMicSessionClosed:"+publishingSessionID)
	assertUserOutboxEvent(t, wrapped.RawDB(), "UserMicLifetimeStatsUpdated:"+publishingSessionID)
}

func TestProcessCrossDayMicSessionKeepsLifetimeTotals(t *testing.T) {
	repo := mysqltest.NewRepository(t).Repository.MicTimeRepository()
	ctx := context.Background()
	base := time.Date(2026, 5, 8, 23, 59, 0, 0, time.UTC).UnixMilli()
	micSessionID := "mic_session_cross_day"

	mustApply(t, repo, roomMicEvent("evt-cross-up", micSessionID, mictimedomain.ActionUp, 10003, 0, 1, base))
	mustApply(t, repo, roomMicEvent("evt-cross-confirm", micSessionID, mictimedomain.ActionPublishConfirmed, 10003, 1, 1, base+30_000))
	mustApply(t, repo, roomMicEvent("evt-cross-down", micSessionID, mictimedomain.ActionDown, 10003, 1, 0, base+2*60_000))

	stats, err := repo.GetLifetimeStats(ctx, "lalu", 10003)
	if err != nil {
		t.Fatalf("GetLifetimeStats failed: %v", err)
	}
	if stats.SeatOccupiedMs != 2*60_000 || stats.MicOnlineMs != 90_000 || stats.SessionCount != 1 {
		t.Fatalf("cross-day lifetime mismatch: %+v", stats)
	}
}

func mustApply(t *testing.T, repo interface {
	ProcessRoomMicEvent(context.Context, mictimedomain.RoomMicEvent) (mictimedomain.ApplyResult, error)
}, event mictimedomain.RoomMicEvent) {
	t.Helper()
	result, err := repo.ProcessRoomMicEvent(context.Background(), event)
	if err != nil || result.Skipped {
		t.Fatalf("ProcessRoomMicEvent failed: event=%+v result=%+v err=%v", event, result, err)
	}
}

func roomMicEvent(eventID string, micSessionID string, action string, userID int64, fromSeat int32, toSeat int32, occurredAtMs int64) mictimedomain.RoomMicEvent {
	event := mictimedomain.RoomMicEvent{
		AppCode:           "lalu",
		EventID:           eventID,
		EventType:         mictimedomain.RoomMicChangedEventType,
		RoomID:            "room-mic-time-test",
		RoomVersion:       occurredAtMs,
		OccurredAtMs:      occurredAtMs,
		ActorUserID:       userID,
		TargetUserID:      userID,
		FromSeat:          fromSeat,
		ToSeat:            toSeat,
		Action:            action,
		MicSessionID:      micSessionID,
		PublishState:      mictimedomain.PublishStatePending,
		PublishDeadlineMs: occurredAtMs + 60_000,
	}
	if action == mictimedomain.ActionPublishConfirmed {
		event.PublishState = mictimedomain.PublishStatePublishing
		event.PublishEventTimeMs = occurredAtMs
	}
	if action == mictimedomain.ActionDown {
		event.PublishState = mictimedomain.PublishStateIdle
	}
	return event
}

func assertUserOutboxEvent(t *testing.T, db *sql.DB, eventID string) {
	t.Helper()
	var count int
	if err := db.QueryRowContext(context.Background(), `
		SELECT COUNT(*)
		FROM user_outbox
		WHERE app_code = 'lalu' AND event_id = ?
	`, eventID).Scan(&count); err != nil {
		t.Fatalf("query user_outbox failed: %v", err)
	}
	if count != 1 {
		t.Fatalf("user_outbox event %s count=%d, want 1", eventID, count)
	}
}

func assertSessionEnded(t *testing.T, db *sql.DB, micSessionID string, reason string) {
	t.Helper()
	var status string
	var endReason string
	if err := db.QueryRowContext(context.Background(), `
		SELECT status, end_reason
		FROM user_mic_sessions
		WHERE app_code = 'lalu' AND mic_session_id = ?
	`, micSessionID).Scan(&status, &endReason); err != nil {
		t.Fatalf("query user_mic_sessions failed: %v", err)
	}
	if status != mictimedomain.SessionStatusEnded || endReason != reason {
		t.Fatalf("session %s terminal state mismatch: status=%s reason=%s", micSessionID, status, endReason)
	}
}
