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

/**
 * Company Auto-Participant rules through the central resolver
 * (docs/TASKS-2026-09-30-AUTO-PARTICIPANTS.md §13): tests D, E, F, I, the
 * membership_lost gate and Q (the per-trip context, D13). Contexts come from
 * the real `loadUserContexts` reading membership_roles / company_memberships /
 * companies in the fake database.
 */

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, resolveAutoParticipantsForUserParticipation } = await import('@/lib/data/auto-participants')
const { suspendCompanyRulesForMember } = await import('@/lib/data/auto-participant-rules')

/* ------------------------------------------------------------ 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 company(id: string, name: string, type = 'carrier') {
  return db.seed('companies', [{ id, display_name: name, legal_name: `${name} LLC`, company_type: type, locked: false }])[0]
}

/** An approved membership with active roles — what `loadUserContexts` turns into contexts. */
function member(userId: string, companyId: string, roles: string[], permission: 'member' | 'company_admin' = 'member') {
  const [m] = db.seed('company_memberships', [{ user_id: userId, company_id: companyId, status: 'approved', permission_level: permission, role: 'carrier_dispatcher' }])
  roles.forEach((role_type, i) => db.seed('membership_roles', [{ membership_id: m.id, user_id: userId, company_id: companyId, role_type, status: 'active', added_at: `2026-01-0${i + 1}T00:00:00.000Z` }]))
  return m
}

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 companyRule(companyId: string, target: { id: string; email: string }, roleType: string | null, scope: string, extra: Row = {}): AutoParticipantRule {
  return db.seed('auto_participant_rules', [{
    rule_type: 'company', owner_company_id: companyId, participant_user_id: target.id, participant_email: target.email, participant_email_normalized: target.email,
    participant_role_type: roleType, scope_type: scope, status: 'active', requires_acceptance: false,
    accepted_at: EFFECTIVE, accepted_by: 'admin-of-company', effective_from: EFFECTIVE, created_by: 'admin-of-company', ...extra,
  }])[0] as unknown as AutoParticipantRule
}

function personalRule(owner: { id: string }, target: { id: string; email: string }): 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_safety_manager', scope_type: 'all_personal_trips', status: 'active', requires_acceptance: true,
    accepted_at: EFFECTIVE, accepted_by: target.id, effective_from: EFFECTIVE, created_by: owner.id,
  }])[0] as unknown as AutoParticipantRule
}

const rowsFor = (userId: string, tripId?: string) => db.table('trip_participants').filter((r) => r.user_id === userId && (!tripId || r.trip_id === tripId))
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 D — company carrier rule', () => {
  it('a dispatcher of X joins as dispatcher → the Safety Manager is added under X', async () => {
    company('X', 'CP Star')
    const m = person('m', 'Mo Dispatcher'), s = person('s', 'Sam Safety')
    member('m', 'X', ['carrier_dispatcher'])
    member('s', 'X', ['carrier_safety_manager'])
    const rule = companyRule('X', s, 'carrier_safety_manager', 'carrier_trips')
    const t = trip()
    const rowM = participant(t.id as string, m)

    const res = await evaluateParticipantRules({ tripId: t.id as string, participant: rowM, trigger: 'participation', session: session('m') })
    expect(res?.outcome).toBe('evaluated')
    expect(res?.rules).toEqual([{ ruleId: rule.id, outcome: 'added', participantId: res?.added[0] }])
    // The trigger's context was resolved silently to its only matching context.
    expect(reload(rowM.id)).toMatchObject({ context_company_id: 'X', context_role_type: 'carrier_dispatcher' })

    const sRows = rowsFor('s')
    expect(sRows).toHaveLength(1)
    expect(sRows[0]).toMatchObject({
      role: 'dispatcher', status: 'active', addition_method: 'company_auto_participant', triggering_company_id: 'X', triggering_user_id: 'm', invited_by: 'm',
      auto_participant_rule_id: rule.id, context_company_id: 'X', context_role_type: 'carrier_safety_manager',
    })
    expect(db.table('trip_participant_sources')[0]).toMatchObject({ participant_id: sRows[0].id, rule_id: rule.id, triggering_company_id: 'X' })
    const email = (mocks.sendPlatformEmail.mock.calls[0] as unknown as [{ templateKey: string; data: Record<string, string> }])[0]
    expect(email.templateKey).toBe('auto_participant_added')
    expect(email.data).toMatchObject({ rule_type: 'company', company_name: 'CP Star' })
  })
})

describe('Test E — brokerage rule follows the role the member acts with (§7, §38)', () => {
  it('joining as Y’s broker adds Operations; joining as X’s dispatcher does not touch Y’s rule', async () => {
    company('X', 'CP Star'), company('Y', 'Big Broker', 'brokerage')
    const m = person('m', 'Mo Many', { default_role: 'broker' }), ops = person('ops', 'Olive Ops', { default_role: 'broker' })
    member('m', 'Y', ['freight_broker'])
    member('m', 'X', ['carrier_dispatcher'])
    member('ops', 'Y', ['freight_broker'])
    const rule = companyRule('Y', ops, 'freight_broker', 'freight_broker_trips')

    const asBroker = trip({ ref_code: 'HH-100000001' })
    const rowBroker = participant(asBroker.id as string, m, { role: 'broker' })
    const r1 = await evaluateParticipantRules({ tripId: asBroker.id as string, participant: rowBroker, trigger: 'participation', session: session('m') })
    expect(r1?.rules).toEqual([{ ruleId: rule.id, outcome: 'added', participantId: r1?.added[0] }])
    expect(reload(rowBroker.id)).toMatchObject({ context_company_id: 'Y', context_role_type: 'freight_broker' })
    expect(rowsFor('ops', asBroker.id as string)[0]).toMatchObject({ role: 'broker', triggering_company_id: 'Y', addition_method: 'company_auto_participant' })

    const asDispatcher = trip({ ref_code: 'HH-100000002' })
    const rowDispatcher = participant(asDispatcher.id as string, m, { role: 'dispatcher' })
    const r2 = await evaluateParticipantRules({ tripId: asDispatcher.id as string, participant: rowDispatcher, trigger: 'participation', session: session('m') })
    expect(r2).toMatchObject({ outcome: 'evaluated', added: [], rules: [] })
    expect(reload(rowDispatcher.id)).toMatchObject({ context_company_id: 'X', context_role_type: 'carrier_dispatcher' })
    expect(rowsFor('ops', asDispatcher.id as string)).toHaveLength(0)
  })
})

describe('Test F — one person, two rules (§19)', () => {
  it('B qualifies through A’s personal rule and X’s company rule → one row, two sources', async () => {
    company('X', 'CP Star')
    const a = person('a', 'Alex'), b = person('b', 'Bea')
    member('a', 'X', ['carrier_dispatcher'])
    member('b', 'X', ['carrier_safety_manager'])
    const personal = personalRule(a, b)
    const corporate = companyRule('X', b, 'carrier_safety_manager', 'carrier_trips')
    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?.added).toHaveLength(1)
    expect(res?.rules.map((r) => [r.ruleId, r.outcome])).toEqual([[personal.id, 'added'], [corporate.id, 'already_present']])
    expect(rowsFor('b')).toHaveLength(1)
    const sources = db.table('trip_participant_sources').filter((s) => s.participant_id === rowsFor('b')[0].id)
    expect(sources.map((s) => s.rule_id).sort()).toEqual([personal.id, corporate.id].sort())
    expect(sources.find((s) => s.rule_id === corporate.id)).toMatchObject({ triggering_company_id: 'X', triggering_user_id: 'a' })
    expect(mocks.sendPlatformEmail).toHaveBeenCalledTimes(1)
  })
})

describe('Test I — membership revoked (§36)', () => {
  it('suspendCompanyRulesForMember suspends the rule; it no longer adds anyone', async () => {
    company('X', 'CP Star')
    const m = person('m', 'Mo'), s = person('s', 'Sam')
    member('m', 'X', ['carrier_dispatcher'])
    member('s', 'X', ['carrier_safety_manager'])
    const rule = companyRule('X', s, 'carrier_safety_manager', 'carrier_trips')

    const n = await suspendCompanyRulesForMember({ userId: 's', companyId: 'X', reason: 'membership_revoked', actor: { id: 'admin-of-company', label: 'Admin' } })
    expect(n).toBe(1)
    const stored = db.table('auto_participant_rules').find((r) => r.id === rule.id)!
    expect(stored).toMatchObject({ status: 'suspended', suspended_reason: 'membership_revoked' })
    expect(db.table('auto_participant_rule_events').filter((e) => e.event === 'rule_suspended' && e.rule_id === rule.id)).toHaveLength(1)

    const t = trip()
    const rowM = participant(t.id as string, m)
    const res = await evaluateParticipantRules({ tripId: t.id as string, participant: rowM, trigger: 'participation', session: session('m') })
    expect(res).toMatchObject({ outcome: 'evaluated', added: [], rules: [] })
    expect(rowsFor('s')).toHaveLength(0)
    // Evaluated directly, the suspended rule is refused by the first gate.
    const direct = await addAutoParticipant({ rule: stored as unknown as AutoParticipantRule, trip: t as never, trigger: reload(rowM.id), 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('s')).toHaveLength(0)
  })
  it('a rule left active after the membership disappeared is skipped as membership_lost', async () => {
    company('X', 'CP Star')
    const m = person('m', 'Mo'), s = person('s', 'Sam')
    member('m', 'X', ['carrier_dispatcher'])
    member('s', 'X', ['carrier_safety_manager'])
    const rule = companyRule('X', s, 'carrier_safety_manager', 'carrier_trips')
    db.tables.company_memberships = db.table('company_memberships').filter((r) => r.user_id !== 's')
    db.tables.membership_roles = db.table('membership_roles').filter((r) => r.user_id !== 's')

    const t = trip()
    const rowM = participant(t.id as string, m)
    const res = await evaluateParticipantRules({ tripId: t.id as string, participant: rowM, trigger: 'participation', session: session('m') })
    expect(res?.rules).toEqual([{ ruleId: rule.id, outcome: 'skipped', reason: 'membership_lost' }])
    expect(rowsFor('s')).toHaveLength(0)
    expect(db.table('auto_participant_rule_events').find((e) => e.event === 'participant_skipped')?.detail).toMatchObject({ reason: 'membership_lost' })
  })
})

describe('Test Q — per-trip context (D13)', () => {
  it('two carrier contexts → context_pending and nothing inserted; after the choice only that company’s rule fires', async () => {
    company('X', 'CP Star'), company('Z', 'Zed Haul')
    const m = person('m', 'Mo Two'), s = person('s', 'Sam of X'), s2 = person('s2', 'Sue of Z')
    member('m', 'X', ['carrier_dispatcher'])
    member('m', 'Z', ['carrier_dispatcher'])
    member('s', 'X', ['carrier_safety_manager'])
    member('s2', 'Z', ['carrier_safety_manager'])
    const ruleX = companyRule('X', s, 'carrier_safety_manager', 'carrier_trips')
    companyRule('Z', s2, 'carrier_safety_manager', 'carrier_trips')
    const t = trip()
    const rowM = participant(t.id as string, m)

    const pending = await resolveAutoParticipantsForUserParticipation({ tripId: t.id as string, participant: rowM, trigger: 'participation', session: session('m') })
    expect(pending).toEqual({ outcome: 'context_pending', added: [], rules: [] })
    expect(db.table('trip_participants')).toHaveLength(1)
    expect(reload(rowM.id)).toMatchObject({ context_company_id: null, context_role_type: null, rules_evaluated_at: null })

    // What POST /api/trips/[id]/context stores before it re-runs the resolver.
    await db.from('trip_participants').update({ context_company_id: 'X', context_role_type: 'carrier_dispatcher' }).eq('id', rowM.id)
    const chosen = await evaluateParticipantRules({ tripId: t.id as string, participant: reload(rowM.id), trigger: 'context_chosen', session: session('m') })
    expect(chosen?.outcome).toBe('evaluated')
    expect(chosen?.rules).toEqual([{ ruleId: ruleX.id, outcome: 'added', participantId: chosen?.added[0] }])
    expect(rowsFor('s')).toHaveLength(1)
    expect(rowsFor('s')[0]).toMatchObject({ triggering_company_id: 'X', context_company_id: 'X' })
    expect(rowsFor('s2')).toHaveLength(0)
    expect(reload(rowM.id).rules_evaluated_at).toEqual(expect.any(String))
  })
})
