import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { createFakeSupabase, type Row } from './stubs/fake-supabase'
import type { AutoParticipantRule, TripParticipant } from '@/types/db'

/**
 * Personal Auto-Participant rules through the central resolver
 * (docs/TASKS-2026-09-30-AUTO-PARTICIPANTS.md §13): tests A, B, C, G, H, K,
 * L, O, P and R. The fake database carries profiles, auth_accounts, trips,
 * trip_participants, auto_participant_rules, trip_participant_sources,
 * auto_participant_rule_events and trip_events; audit, operation events and
 * email are mocked.
 */

const db = createFakeSupabase()
const mocks = vi.hoisted(() => ({
  logTripEvent: vi.fn(async () => {}),
  logOperationEvent: vi.fn(async () => {}),
  sendPlatformEmail: vi.fn(async () => ({ status: 'sent' })),
}))
vi.mock('@/lib/supabase/admin', () => ({ createAdminClient: () => db }))
vi.mock('@/lib/audit', () => ({ logTripEvent: mocks.logTripEvent }))
vi.mock('@/lib/observability/operation-events', () => ({ logOperationEvent: mocks.logOperationEvent }))
vi.mock('@/lib/email/send', () => ({ sendPlatformEmail: mocks.sendPlatformEmail }))

const { addAutoParticipant, evaluateParticipantRules, evaluateParticipantRulesById, resolveAutoParticipantsForUserParticipation } = await import('@/lib/data/auto-participants')

/* ------------------------------------------------------------ fixtures */

const EFFECTIVE = '2026-01-01T00:00:00.000Z'

function person(id: string, name: string, extra: Row = {}) {
  const email = `${id}@users.test`
  db.seed('profiles', [{ id, email, full_name: name, default_role: 'dispatcher', claim_status: null, ...extra }])
  db.seed('auth_accounts', [{ user_id: id }])
  return { id, email, name }
}

function trip(extra: Row = {}) {
  return db.seed('trips', [{ ref_code: 'HH-100000001', origin: 'Houston, TX', destination: 'Dallas, TX', carrier_name: 'Test Carrier', ...extra }])[0]
}

function participant(tripId: string, p: { id: string; email: string; name: string }, extra: Row = {}): TripParticipant {
  return db.seed('trip_participants', [{ trip_id: tripId, user_id: p.id, email: p.email, name: p.name, role: 'dispatcher', status: 'active', addition_method: 'trip_creator', ...extra }])[0] as unknown as TripParticipant
}

function personalRule(owner: { id: string }, target: { id: string; email: string }, extra: Row = {}): AutoParticipantRule {
  return db.seed('auto_participant_rules', [{
    rule_type: 'personal', owner_user_id: owner.id, participant_user_id: target.id, participant_email: target.email, participant_email_normalized: target.email,
    participant_role_type: 'carrier_dispatcher', scope_type: 'all_personal_trips', status: 'active', requires_acceptance: true,
    accepted_at: EFFECTIVE, accepted_by: target.id, effective_from: EFFECTIVE, created_by: owner.id, ...extra,
  }])[0] as unknown as AutoParticipantRule
}

const rowsFor = (userId: string) => db.table('trip_participants').filter((r) => r.user_id === userId)
const session = (id: string) => ({ id, originId: id, preview: null })
const reload = (id: string) => db.table('trip_participants').find((r) => r.id === id) as unknown as TripParticipant

beforeEach(() => db.reset())
afterEach(() => vi.clearAllMocks())

/* --------------------------------------------------------------- tests */

describe('Test A — personal creator rule', () => {
  it('A creates a trip → B is added exactly once with full provenance', async () => {
    const a = person('a', 'Alex'), b = person('b', 'Bea')
    const rule = personalRule(a, b)
    const t = trip()
    const rowA = participant(t.id as string, a)

    const res = await evaluateParticipantRules({ tripId: t.id as string, participant: rowA, trigger: 'participation', session: session('a') })
    expect(res?.outcome).toBe('evaluated')
    expect(res?.added).toHaveLength(1)
    expect(res?.rules).toEqual([{ ruleId: rule.id, outcome: 'added', participantId: res?.added[0] }])

    const bRows = rowsFor('b')
    expect(bRows).toHaveLength(1)
    expect(bRows[0]).toMatchObject({
      trip_id: t.id, email: b.email, name: 'Bea', role: 'dispatcher', status: 'active',
      addition_method: 'personal_auto_participant', triggering_user_id: 'a', triggering_company_id: null, invited_by: 'a',
      auto_participant_rule_id: rule.id, context_company_id: null, context_role_type: 'carrier_dispatcher',
    })
    expect(db.table('trip_participant_sources')).toHaveLength(1)
    expect(db.table('trip_participant_sources')[0]).toMatchObject({ participant_id: bRows[0].id, rule_id: rule.id, triggering_user_id: 'a' })
    // The creator's own row remembers the evaluation and its context (D12, D13).
    expect(reload(rowA.id)).toMatchObject({ rules_evaluated_at: expect.any(String), context_role_type: 'carrier_dispatcher', context_company_id: null })
    expect(db.table('auto_participant_rule_events').filter((e) => e.event === 'participant_added')).toHaveLength(1)
  })
})

describe('Test B — personal invited rule (D12)', () => {
  it('an invited A row fires the rule; the later activation of the same row does not fire again', async () => {
    const a = person('a', 'Alex'), b = person('b', 'Bea')
    personalRule(a, b)
    const t = trip()
    const rowA = participant(t.id as string, a, { status: 'invited', addition_method: 'manual_invite' })

    const first = await evaluateParticipantRules({ tripId: t.id as string, participant: rowA, trigger: 'participation', session: null })
    expect(first?.outcome).toBe('evaluated')
    expect(rowsFor('b')).toHaveLength(1)
    expect(rowsFor('b')[0].status).toBe('active')

    // Accept path: the row becomes active. Re-evaluation is a no-op.
    await db.from('trip_participants').update({ status: 'active' }).eq('id', rowA.id)
    const second = await evaluateParticipantRulesById({ tripId: t.id as string, participantId: rowA.id, trigger: 'participation', session: session('a') })
    expect(second?.outcome).toBe('already_evaluated')
    expect(rowsFor('b')).toHaveLength(1)
  })
})

describe('Test C — no recursion (§5)', () => {
  it('A→B and B→C: A joining adds B only; the resolver on B’s auto-added row does nothing', async () => {
    const a = person('a', 'Alex'), b = person('b', 'Bea'), c = person('c', 'Cal')
    personalRule(a, b)
    personalRule(b, c)
    const t = trip()
    const rowA = participant(t.id as string, a)

    await evaluateParticipantRules({ tripId: t.id as string, participant: rowA, trigger: 'participation', session: session('a') })
    expect(rowsFor('b')).toHaveLength(1)
    expect(rowsFor('c')).toHaveLength(0)

    const rowB = reload(rowsFor('b')[0].id as string)
    const res = await resolveAutoParticipantsForUserParticipation({ tripId: t.id as string, participant: rowB, trigger: 'participation', session: null })
    expect(res.outcome).toBe('not_applicable')
    expect(res.added).toEqual([])
    expect(rowsFor('c')).toHaveLength(0)
  })
})

describe('Test G — disabled rule', () => {
  it('is not loaded by the resolver and, evaluated directly, is skipped as rule_not_active', async () => {
    const a = person('a', 'Alex'), b = person('b', 'Bea')
    const rule = personalRule(a, b, { status: 'disabled', disabled_at: EFFECTIVE, disabled_by: 'a' })
    const t = trip()
    const rowA = participant(t.id as string, a)

    const res = await evaluateParticipantRules({ tripId: t.id as string, participant: rowA, trigger: 'participation', session: session('a') })
    expect(res).toMatchObject({ outcome: 'evaluated', added: [], rules: [] })
    expect(rowsFor('b')).toHaveLength(0)

    const direct = await addAutoParticipant({ rule, trip: t as never, trigger: rowA, triggerKind: 'participation', at: new Date().toISOString(), session: null, admin: db as never })
    expect(direct).toEqual({ ruleId: rule.id, outcome: 'skipped', reason: 'rule_not_active' })
    expect(rowsFor('b')).toHaveLength(0)
    const skipped = db.table('auto_participant_rule_events').filter((e) => e.event === 'participant_skipped')
    expect(skipped).toHaveLength(1)
    expect(skipped[0].detail).toMatchObject({ reason: 'rule_not_active', trigger_participant: rowA.id })
  })
})

describe('Test H — historical / prospective (§30, D10)', () => {
  it('a participation before effective_from is skipped', async () => {
    const a = person('a', 'Alex'), b = person('b', 'Bea')
    const rule = personalRule(a, b, { effective_from: '2999-01-01T00:00:00.000Z' })
    const t = trip()
    const rowA = participant(t.id as string, a)
    const res = await evaluateParticipantRules({ tripId: t.id as string, participant: rowA, trigger: 'participation', session: session('a') })
    expect(res?.rules).toEqual([{ ruleId: rule.id, outcome: 'skipped', reason: 'before_effective_from' }])
    expect(rowsFor('b')).toHaveLength(0)
  })
  it('a claim compares the trip’s imported_at with effective_from, not the claim time', async () => {
    const a = person('a', 'Alex'), b = person('b', 'Bea')
    const rule = personalRule(a, b, { effective_from: '2026-05-01T00:00:00.000Z' })
    const older = trip({ ref_code: 'HH-20000001', imported_at: '2026-01-15T00:00:00.000Z', created_at: '2026-01-10T00:00:00.000Z' })
    const newer = trip({ ref_code: 'HH-20000002', imported_at: '2026-06-15T00:00:00.000Z', created_at: '2026-06-10T00:00:00.000Z' })
    const rowOld = participant(older.id as string, a, { addition_method: 'synchron_import' })
    const rowNew = participant(newer.id as string, a, { addition_method: 'synchron_import' })

    const r1 = await evaluateParticipantRules({ tripId: older.id as string, participant: rowOld, trigger: 'claim', session: null })
    expect(r1?.rules).toEqual([{ ruleId: rule.id, outcome: 'skipped', reason: 'before_effective_from' }])
    const r2 = await evaluateParticipantRules({ tripId: newer.id as string, participant: rowNew, trigger: 'claim', session: null })
    expect(r2?.rules[0]).toMatchObject({ ruleId: rule.id, outcome: 'added' })
    expect(rowsFor('b').map((r) => r.trip_id)).toEqual([newer.id])
  })
})

describe('Test K — admin preview (§39)', () => {
  it('a switched or previewing session evaluates nothing and inserts nothing', async () => {
    const a = person('a', 'Alex'), b = person('b', 'Bea')
    personalRule(a, b)
    const t = trip()
    const rowA = participant(t.id as string, a)

    const switched = await resolveAutoParticipantsForUserParticipation({ tripId: t.id as string, participant: rowA, trigger: 'participation', session: { id: 'a', originId: 'admin-1', preview: null } })
    expect(switched).toEqual({ outcome: 'not_applicable', added: [], rules: [] })
    const previewing = await resolveAutoParticipantsForUserParticipation({ tripId: t.id as string, participant: rowA, trigger: 'participation', session: { id: 'a', originId: 'a', preview: 'carrier' } })
    expect(previewing).toEqual({ outcome: 'not_applicable', added: [], rules: [] })
    expect(rowsFor('b')).toHaveLength(0)
    expect(db.table('trip_participant_sources')).toHaveLength(0)
    expect(reload(rowA.id).rules_evaluated_at).toBeNull()
  })
})

describe('Test L — existing manual participant (§18)', () => {
  it('B already invited → no second row, one source row, no email', async () => {
    const a = person('a', 'Alex'), b = person('b', 'Bea')
    const rule = personalRule(a, b)
    const t = trip()
    const manualB = participant(t.id as string, b, { status: 'invited', addition_method: 'manual_invite', invited_by: 'someone-else' })
    const rowA = participant(t.id as string, a)

    const res = await evaluateParticipantRules({ tripId: t.id as string, participant: rowA, trigger: 'participation', session: session('a') })
    expect(res?.rules).toEqual([{ ruleId: rule.id, outcome: 'already_present', participantId: manualB.id }])
    expect(res?.added).toEqual([])
    expect(rowsFor('b')).toHaveLength(1)
    expect(rowsFor('b')[0]).toMatchObject({ status: 'invited', addition_method: 'manual_invite' })
    expect(db.table('trip_participant_sources')).toHaveLength(1)
    expect(db.table('trip_participant_sources')[0]).toMatchObject({ participant_id: manualB.id, rule_id: rule.id, triggering_user_id: 'a' })
    expect(db.table('auto_participant_rule_events').filter((e) => e.event === 'participant_already_present')).toHaveLength(1)
    expect(mocks.sendPlatformEmail).not.toHaveBeenCalled()
  })
})

describe('Test O — auto-added email (D8)', () => {
  it('sends auto_participant_added once per added person', async () => {
    const a = person('a', 'Alex Carrier'), b = person('b', 'Bea Safety')
    personalRule(a, b)
    const t = trip()
    const rowA = participant(t.id as string, a)
    await evaluateParticipantRules({ tripId: t.id as string, participant: rowA, trigger: 'participation', session: session('a') })

    expect(mocks.sendPlatformEmail).toHaveBeenCalledTimes(1)
    const call = (mocks.sendPlatformEmail.mock.calls[0] as unknown as [{ templateKey: string; to: string; userId: string; data: Record<string, string> }])[0]
    expect(call.templateKey).toBe('auto_participant_added')
    expect(call.to).toBe(b.email)
    expect(call.userId).toBe('b')
    expect(call.data).toMatchObject({ first_name: 'Bea', trip_id: 'HH-100000001', trip_route: 'Houston, TX → Dallas, TX', rule_type: 'personal', triggering_user: 'Alex Carrier' })
    expect(call.data.workspace_link).toContain(`/trips/${t.id}`)
  })
})

describe('Test P — fire once (D12)', () => {
  it('a second evaluation of the same row answers already_evaluated', async () => {
    const a = person('a', 'Alex'), b = person('b', 'Bea')
    personalRule(a, b)
    const t = trip()
    const rowA = participant(t.id as string, a, { status: 'invited', addition_method: 'manual_invite' })

    const first = await evaluateParticipantRules({ tripId: t.id as string, participant: rowA, trigger: 'participation', session: null })
    expect(first?.outcome).toBe('evaluated')
    const second = await evaluateParticipantRulesById({ tripId: t.id as string, participantId: rowA.id, trigger: 'participation', session: null })
    expect(second?.outcome).toBe('already_evaluated')
    expect(rowsFor('b')).toHaveLength(1)
    expect(db.table('trip_participant_sources')).toHaveLength(1)
    expect(mocks.sendPlatformEmail).toHaveBeenCalledTimes(1)
  })
})

describe('Test R — pilot trigger (D1, D14)', () => {
  it('a pilot limited to TX adds a dispatcher-role person as a pilot with the same limit', async () => {
    const p = person('p', 'Pat Pilot'), b = person('b', 'Bea')
    const rule = personalRule(p, b) // B's own role is carrier_dispatcher
    const t = trip()
    db.seed('trip_events', [{ trip_id: t.id, actor_id: null, actor_label: 'Alex', action: 'participant_invited', detail: { email: p.email, role: 'pilot', pilot_access: 'States: TX' } }])
    const rowP = participant(t.id as string, p, { role: 'pilot', addition_method: 'manual_invite', invited_by: 'a' })

    const res = await evaluateParticipantRules({ tripId: t.id as string, participant: rowP, trigger: 'participation', session: session('p') })
    expect(res?.rules[0]).toMatchObject({ ruleId: rule.id, outcome: 'added' })
    const bRows = rowsFor('b')
    expect(bRows).toHaveLength(1)
    expect(bRows[0]).toMatchObject({ role: 'pilot', status: 'active', addition_method: 'personal_auto_participant', triggering_user_id: 'p', invited_by: 'p' })

    const added = mocks.logTripEvent.mock.calls.map((c) => (c as unknown as [{ action: string; detail: Record<string, unknown> }])[0]).find((c) => c.action === 'participant_auto_added')
    expect(added).toBeDefined()
    expect(added?.detail).toMatchObject({ email: b.email, role: 'pilot', participant_id: bRows[0].id, rule_id: rule.id, triggering_user_id: 'p', pilot_access: 'States: TX' })
  })
})
