package seatbilling

import (
	"context"
	"fmt"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/coredb"
)

func TestListActivePlansReturnsSelfServiceTeamAndEnterprisePeriods(t *testing.T) {
	t.Parallel()

	ctx := context.Background()
	db := startSeatBillingRollbackDB(t, ctx)
	store := New(db)

	plans, err := store.ListActivePlans(ctx)
	require.NoError(t, err)

	require.Len(t, plans, 6)
	require.Equal(t, "team", plans[0].PlanKey)
	require.Equal(t, "month", plans[0].BillingPeriod)
	require.EqualValues(t, 10, plans[0].ManagedAgentsPerSeat)
	require.Equal(t, 1, plans[0].MinSeatQuantity)
	require.Equal(t, 50, plans[0].MaxSeatQuantity)
	require.EqualValues(t, 50000, plans[0].UnitPriceCents)
	require.True(t, plans[0].SelfServiceEnabled)
	require.Equal(t, "enterprise", plans[3].PlanKey)
	require.Equal(t, "month", plans[3].BillingPeriod)
	require.EqualValues(t, 20, plans[3].ManagedAgentsPerSeat)
	require.Equal(t, 0, plans[3].MaxSeatQuantity)
	require.EqualValues(t, 100000, plans[3].UnitPriceCents)
	for _, plan := range plans {
		require.NotEqual(t, "max", plan.PlanKey)
		require.Equal(t, "active", plan.Status)
		require.NotZero(t, plan.UnitPriceCents)
		require.Equal(t, "CNY", plan.Currency)
	}
}

func TestLoadSelfServicePlanRejectsInactiveMaxPlan(t *testing.T) {
	t.Parallel()

	ctx := context.Background()
	db := startSeatBillingRollbackDB(t, ctx)
	store := New(db)

	plan, err := store.LoadSelfServicePlan(ctx, "enterprise", "year")
	require.NoError(t, err)
	require.Equal(t, "enterprise", plan.PlanKey)
	require.Equal(t, "year", plan.BillingPeriod)
	require.Equal(t, 0, plan.MaxSeatQuantity)
	require.EqualValues(t, 1200000, plan.UnitPriceCents)

	_, err = store.LoadSelfServicePlan(ctx, "max", "year")
	require.Error(t, err)
}

func TestLoadEffectiveEntitlementUsesActiveSubscriptionSnapshots(t *testing.T) {
	t.Parallel()

	ctx := context.Background()
	db := startSeatBillingRollbackDB(t, ctx)
	store := New(db)
	now := time.Date(2026, 6, 8, 10, 0, 0, 0, time.UTC)
	seedSeatBillingOrganization(t, ctx, db, "org_seat_active", now)
	seedSeatSubscription(t, ctx, db, "org_seat_active", "active", "team", "month", 4, 10, 10, 3900, now.Add(-24*time.Hour), now.Add(30*24*time.Hour), now)

	entitlement, err := store.LoadEffectiveEntitlement(ctx, "org_seat_active", now)
	require.NoError(t, err)
	require.True(t, entitlement.Paid)
	require.Equal(t, "team", entitlement.PlanKey)
	require.Equal(t, "month", entitlement.BillingPeriod)
	require.EqualValues(t, FreeHumanSeatLimit+4, entitlement.HumanSeatLimit)
	require.EqualValues(t, 10, entitlement.OrganizationAgentLimit)
	require.EqualValues(t, 10, entitlement.ManagedAgentsPerSeat)
	require.NotNil(t, entitlement.CurrentPeriodStart)
	require.NotNil(t, entitlement.CurrentPeriodEnd)
}

func TestLoadEffectiveEntitlementFallsBackToFreeForExpiredOrSuspendedSubscription(t *testing.T) {
	t.Parallel()

	ctx := context.Background()
	db := startSeatBillingRollbackDB(t, ctx)
	store := New(db)
	now := time.Date(2026, 6, 8, 10, 0, 0, 0, time.UTC)
	seedSeatBillingOrganization(t, ctx, db, "org_seat_free", now)

	entitlement, err := store.LoadEffectiveEntitlement(ctx, "org_seat_free", now)
	require.NoError(t, err)
	require.False(t, entitlement.Paid)
	require.Equal(t, "free", entitlement.PlanKey)
	require.EqualValues(t, FreeHumanSeatLimit, entitlement.HumanSeatLimit)
	require.EqualValues(t, FreeAgentsPerHumanOwnerLimit, entitlement.OrganizationAgentLimit)
	require.EqualValues(t, FreeAgentsPerHumanOwnerLimit, entitlement.ManagedAgentsPerSeat)

	seedSeatSubscription(t, ctx, db, "org_seat_free", "active", "enterprise", "month", 8, 20, 20, 9900, now.Add(-40*24*time.Hour), now.Add(-24*time.Hour), now)
	entitlement, err = store.LoadEffectiveEntitlement(ctx, "org_seat_free", now)
	require.NoError(t, err)
	require.False(t, entitlement.Paid)
	require.EqualValues(t, FreeHumanSeatLimit, entitlement.HumanSeatLimit)
	require.EqualValues(t, FreeAgentsPerHumanOwnerLimit, entitlement.OrganizationAgentLimit)
	require.EqualValues(t, FreeAgentsPerHumanOwnerLimit, entitlement.ManagedAgentsPerSeat)

	seedSeatSubscription(t, ctx, db, "org_seat_free", "suspended", "enterprise", "month", 8, 20, 20, 9900, now.Add(-24*time.Hour), now.Add(30*24*time.Hour), now)
	entitlement, err = store.LoadEffectiveEntitlement(ctx, "org_seat_free", now)
	require.NoError(t, err)
	require.False(t, entitlement.Paid)
	require.EqualValues(t, FreeHumanSeatLimit, entitlement.HumanSeatLimit)
	require.EqualValues(t, FreeAgentsPerHumanOwnerLimit, entitlement.OrganizationAgentLimit)
	require.EqualValues(t, FreeAgentsPerHumanOwnerLimit, entitlement.ManagedAgentsPerSeat)
}

func TestLoadSeatSummaryReturnsUsageAndOverLimitFlags(t *testing.T) {
	t.Parallel()

	ctx := context.Background()
	db := startSeatBillingRollbackDB(t, ctx)
	store := New(db)
	now := time.Date(2026, 6, 8, 10, 0, 0, 0, time.UTC)
	seedSeatBillingOrganization(t, ctx, db, "org_seat_summary", now)
	for index := 0; index < int(FreeHumanSeatLimit)+1; index++ {
		seedSeatBillingOrganizationMember(t, ctx, db, "org_seat_summary", index, now)
	}
	seedSeatBillingWorkspace(t, ctx, db, "org_seat_summary", "wsp_seat_summary", now)
	seedSeatBillingMachine(t, ctx, db, "wsp_seat_summary", "mac_seat_summary_owner", "usr_org_seat_summary", now)
	seedSeatBillingMachine(t, ctx, db, "wsp_seat_summary", "mac_seat_summary_peer", "usr_org_seat_summary_0", now)
	for index := 0; index < int(FreeAgentsPerHumanOwnerLimit); index++ {
		seedSeatBillingAgent(t, ctx, db, "wsp_seat_summary", "mac_seat_summary_owner", index, now)
	}
	seedSeatBillingAgent(t, ctx, db, "wsp_seat_summary", "mac_seat_summary_peer", 100, now)

	summary, err := store.LoadSeatSummary(ctx, "org_seat_summary", now)
	require.NoError(t, err)
	require.False(t, summary.Entitlement.Paid)
	require.EqualValues(t, FreeHumanSeatLimit, summary.Entitlement.HumanSeatLimit)
	require.EqualValues(t, FreeAgentsPerHumanOwnerLimit, summary.Entitlement.OrganizationAgentLimit)
	require.EqualValues(t, FreeAgentsPerHumanOwnerLimit, summary.Entitlement.ManagedAgentsPerSeat)
	require.EqualValues(t, FreeHumanSeatLimit+1, summary.CurrentHumanSeats)
	require.EqualValues(t, FreeAgentsPerHumanOwnerLimit, summary.CurrentAgents)
	require.True(t, summary.HumanSeatsOverLimit)
	require.False(t, summary.AgentsOverLimit)

	seedSeatBillingAgent(t, ctx, db, "wsp_seat_summary", "mac_seat_summary_owner", 200, now)

	summary, err = store.LoadSeatSummary(ctx, "org_seat_summary", now)
	require.NoError(t, err)
	require.EqualValues(t, FreeAgentsPerHumanOwnerLimit+1, summary.CurrentAgents)
	require.True(t, summary.AgentsOverLimit)

	seedAgentOwnerGrant(t, ctx, db, "grant_seat_summary_agent", "org_seat_summary", 1, ptrTime(now.Add(30*24*time.Hour)), nil, now.Add(-time.Hour))
	summary, err = store.LoadSeatSummary(ctx, "org_seat_summary", now)
	require.NoError(t, err)
	require.EqualValues(t, FreeAgentsPerHumanOwnerLimit+1, summary.Entitlement.OrganizationAgentLimit)
	require.EqualValues(t, FreeAgentsPerHumanOwnerLimit+1, summary.CurrentAgents)
	require.False(t, summary.AgentsOverLimit)
}

func seedSeatBillingOrganization(t *testing.T, ctx context.Context, db coredb.DB, organizationID string, now time.Time) {
	t.Helper()
	userID := "usr_" + organizationID
	mustSeatBillingExec(t, ctx, db, `
INSERT INTO users (id, email, name, display_name, password_hash, is_admin, created_at, updated_at)
VALUES ($1, $2, $3, $4, 'hash', FALSE, $5, $5)
`, userID, organizationID+"@example.com", userID, userID, now)
	mustSeatBillingExec(t, ctx, db, `
INSERT INTO organizations (id, slug, name, created_by_user_id, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $5)
`, organizationID, organizationID, organizationID, userID, now)
}

func seedSeatBillingOrganizationMember(t *testing.T, ctx context.Context, db coredb.DB, organizationID string, index int, now time.Time) {
	t.Helper()
	userID := fmt.Sprintf("usr_%s_%d", organizationID, index)
	memberID := fmt.Sprintf("orgm_%s_%d", organizationID, index)
	mustSeatBillingExec(t, ctx, db, `
INSERT INTO users (id, email, name, display_name, password_hash, is_admin, created_at, updated_at)
VALUES ($1, $2, $3, $3, 'hash', FALSE, $4, $4)
`, userID, userID+"@example.com", userID, now)
	mustSeatBillingExec(t, ctx, db, `
INSERT INTO organization_members (id, organization_id, user_id, org_role, status, source, joined_at, created_at, updated_at)
VALUES ($1, $2, $3, 'org_member', 'active', 'local', $4, $4, $4)
`, memberID, organizationID, userID, now)
}

func seedSeatBillingWorkspace(t *testing.T, ctx context.Context, db coredb.DB, organizationID string, workspaceID string, now time.Time) {
	t.Helper()
	mustSeatBillingExec(t, ctx, db, `
INSERT INTO workspaces (id, organization_id, slug, name, latest_message_seq, created_at, updated_at)
VALUES ($1, $2, $3, $3, 0, $4, $4)
`, workspaceID, organizationID, workspaceID, now)
}

func seedSeatBillingMachine(t *testing.T, ctx context.Context, db coredb.DB, workspaceID string, machineID string, createdByUserID string, now time.Time) {
	t.Helper()
	mustSeatBillingExec(t, ctx, db, `
INSERT INTO machines (id, workspace_id, created_by_user_id, name, api_key, status, created_at, updated_at)
VALUES ($1, $2, $3, $1, $1, 'active', $4, $4)
`, machineID, workspaceID, createdByUserID, now)
}

func seedSeatBillingAgent(t *testing.T, ctx context.Context, db coredb.DB, workspaceID string, machineID string, index int, now time.Time) {
	t.Helper()
	memberID := fmt.Sprintf("mem_seat_%d", index)
	agentID := fmt.Sprintf("agt_seat_%d", index)
	mustSeatBillingExec(t, ctx, db, `
INSERT INTO members (id, workspace_id, type, display_name, status, created_at, updated_at)
VALUES ($1, $2, 'agent', $1, 'active', $3, $3)
`, memberID, workspaceID, now)
	mustSeatBillingExec(t, ctx, db, `
INSERT INTO agents (id, workspace_id, machine_id, member_id, name, runtime, model, description, auth_token_hash, status, activity_detail, created_at, updated_at)
VALUES ($1, $2, $3, $4, $1, 'codex', 'gpt-5', '', 'hash', 'inactive', '', $5, $5)
`, agentID, workspaceID, machineID, memberID, now)
}

func seedSeatSubscription(t *testing.T, ctx context.Context, db coredb.DB, organizationID string, status string, planKey string, billingPeriod string, seatQuantity int, managedAgentsPerSeat int64, maxHumanSeats int, unitPriceCents int64, startsAt time.Time, endsAt time.Time, now time.Time) {
	t.Helper()
	mustSeatBillingExec(t, ctx, db, `
INSERT INTO organization_seat_subscriptions (
  organization_id,
  status,
  plan_key,
  billing_period,
  seat_quantity,
  managed_agents_per_seat_snapshot,
  max_human_seats_snapshot,
  unit_price_cents_snapshot,
  currency,
  current_period_start,
  current_period_end,
  created_at,
  updated_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'CNY', $9, $10, $11, $11)
ON DUPLICATE KEY UPDATE
  status = VALUES(status),
  plan_key = VALUES(plan_key),
  billing_period = VALUES(billing_period),
  seat_quantity = VALUES(seat_quantity),
  managed_agents_per_seat_snapshot = VALUES(managed_agents_per_seat_snapshot),
  max_human_seats_snapshot = VALUES(max_human_seats_snapshot),
  unit_price_cents_snapshot = VALUES(unit_price_cents_snapshot),
  current_period_start = VALUES(current_period_start),
  current_period_end = VALUES(current_period_end),
  updated_at = VALUES(updated_at)
`, organizationID, status, planKey, billingPeriod, seatQuantity, managedAgentsPerSeat, maxHumanSeats, unitPriceCents, startsAt, endsAt, now)
}

func mustSeatBillingExec(t *testing.T, ctx context.Context, db coredb.DB, query string, args ...any) {
	t.Helper()
	_, err := db.Exec(ctx, query, args...)
	require.NoError(t, err)
}

func TestTimedSeatGrantsContributeToEffectiveEntitlement(t *testing.T) {
	t.Parallel()

	ctx := context.Background()
	db := startSeatBillingRollbackDB(t, ctx)
	store := New(db)
	now := time.Date(2026, 6, 25, 9, 0, 0, 0, time.UTC)
	seedSeatBillingOrganization(t, ctx, db, "org_seat_grants", now)
	seedSeatGrant(t, ctx, db, "grant_active", "org_seat_grants", 2, ptrTime(now.Add(30*24*time.Hour)), nil, now.Add(-time.Hour))
	seedSeatGrant(t, ctx, db, "grant_permanent", "org_seat_grants", 3, nil, nil, now.Add(-time.Hour))
	seedSeatGrant(t, ctx, db, "grant_expired", "org_seat_grants", 9, ptrTime(now.Add(-24*time.Hour)), nil, now.Add(-48*time.Hour))
	seedSeatGrant(t, ctx, db, "grant_revoked", "org_seat_grants", 9, ptrTime(now.Add(30*24*time.Hour)), ptrTime(now.Add(-30*time.Minute)), now.Add(-time.Hour))

	entitlement, err := store.LoadEffectiveEntitlement(ctx, "org_seat_grants", now)
	require.NoError(t, err)
	require.EqualValues(t, FreeHumanSeatLimit+5, entitlement.HumanSeatLimit)
	require.EqualValues(t, FreeHumanSeatLimit, entitlement.FreeHumanSeatLimit)
	require.EqualValues(t, 5, entitlement.GiftHumanSeatLimit)
	require.NotNil(t, entitlement.NextGrantExpiresAt)
	require.WithinDuration(t, now.Add(30*24*time.Hour), *entitlement.NextGrantExpiresAt, time.Second)

	entitlement, err = store.LoadEffectiveEntitlement(ctx, "org_seat_grants", now.Add(31*24*time.Hour))
	require.NoError(t, err)
	require.EqualValues(t, FreeHumanSeatLimit+3, entitlement.HumanSeatLimit)
	require.EqualValues(t, 3, entitlement.GiftHumanSeatLimit)
	require.Nil(t, entitlement.NextGrantExpiresAt)
}

func seedSeatGrant(t *testing.T, ctx context.Context, db coredb.DB, grantID string, organizationID string, seatDelta int64, expiresAt *time.Time, revokedAt *time.Time, createdAt time.Time) {
	t.Helper()
	mustSeatBillingExec(t, ctx, db, `
INSERT INTO organization_resource_quota_grants (
  id,
  organization_id,
  metric,
  quota_scope,
  limit_delta,
  expires_at,
  source_type,
  source_ref,
  request_id,
  reason,
  created_by_operator,
  revoked_at,
  revoke_reason,
  created_at,
  updated_at
) VALUES ($1, $2, 'seats', 'organization', $3, $4, 'operator_manual', '', $5, 'launch gift', $6, $7, '', $8, $8)
`, grantID, organizationID, seatDelta, expiresAt, grantID+"_request", "usr_"+organizationID, revokedAt, createdAt)
}

func seedAgentOwnerGrant(t *testing.T, ctx context.Context, db coredb.DB, grantID string, organizationID string, agentDelta int64, expiresAt *time.Time, revokedAt *time.Time, createdAt time.Time) {
	t.Helper()
	mustSeatBillingExec(t, ctx, db, `
INSERT INTO organization_resource_quota_grants (
  id,
  organization_id,
  metric,
  quota_scope,
  limit_delta,
  expires_at,
  source_type,
  source_ref,
  request_id,
  reason,
  created_by_operator,
  revoked_at,
  revoke_reason,
  created_at,
  updated_at
) VALUES ($1, $2, 'agents', 'agent_owner', $3, $4, 'operator_manual', '', $5, 'campaign grant', $6, $7, '', $8, $8)
`, grantID, organizationID, agentDelta, expiresAt, grantID+"_request", "usr_"+organizationID, revokedAt, createdAt)
}

func ptrTime(value time.Time) *time.Time {
	return &value
}
