import { randomUUID } from 'node:crypto'
import { NextRequest } from 'next/server'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { createFakeSupabase, type Row } from './stubs/fake-supabase'
import type { AutoParticipantRule, TripParticipant } from '@/types/db'

/**
 * Auto-Participants at the API boundary (docs/TASKS-2026-09-30-AUTO-PARTICIPANTS.md §13):
 *   J — requireCompanyAdminContext (Company Admin is a permission, not a role)
 *   M — POST /api/internal/auto-participants/evaluate (bearer CRON_SECRET)
 *   T — self stop (D16) by a third party
 *   U — removing an auto-added participant from a trip (D17)
 */

const db = createFakeSupabase()
const mocks = vi.hoisted(() => ({
  logTripEvent: vi.fn(async () => {}),
  logOperationEvent: vi.fn(async () => {}),
  sendPlatformEmail: vi.fn(async () => ({ status: 'sent' })),
  getSessionUser: vi.fn<() => Promise<unknown>>(async () => null),
  getMyParticipant: vi.fn(async () => null),
  loadActiveMode: vi.fn<() => Promise<unknown>>(async () => null),
  evaluateParticipantRulesById: vi.fn(async () => ({ outcome: 'evaluated', added: ['new-row'], rules: [] })),
}))
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 }))
vi.mock('@/lib/auth', () => ({ getSessionUser: mocks.getSessionUser, getMyParticipant: mocks.getMyParticipant }))
vi.mock('@/lib/data/active-mode', () => ({ loadActiveMode: mocks.loadActiveMode }))
vi.mock('@/lib/data/auto-participants', () => ({ evaluateParticipantRulesById: mocks.evaluateParticipantRulesById }))

const { requireCompanyAdminContext } = await import('@/lib/api-guard')
const { POST: evaluate } = await import('@/app/api/internal/auto-participants/evaluate/route')
const { respondToRule } = await import('@/lib/data/auto-participant-rules')
const { removeParticipantFromTrip } = await import('@/lib/data/participant-removal')

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

const MEMBER = { id: 'm', email: 'm@users.test', name: 'Mo Member', role: 'dispatcher', company: null, internal: false, originId: 'm' }
const PLATFORM_ADMIN = { id: 'adm', email: 'adm@users.test', name: 'Ada', role: 'admin', company: null, internal: true, originId: 'adm' }

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, extra: Row = {}) {
  return db.seed('companies', [{ id, display_name: name, legal_name: `${name} LLC`, company_type: 'carrier', locked: false, ...extra }])[0]
}
function membership(userId: string, companyId: string, permission: 'member' | 'company_admin', status = 'approved') {
  return db.seed('company_memberships', [{ user_id: userId, company_id: companyId, status, permission_level: permission, role: 'carrier_dispatcher' }])[0]
}
function activeMode(companyId: string | null, permission: 'member' | 'company_admin') {
  return { active: companyId ? { key: 'ctx', roleType: 'carrier_dispatcher', companyId, companyName: 'CP Star', permission, status: 'active' } : null }
}
function trip() {
  return db.seed('trips', [{ ref_code: 'HH-100000001', origin: 'Houston, TX', destination: 'Dallas, TX', carrier_name: 'Test Carrier' }])[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 rule(extra: Row): AutoParticipantRule {
  return db.seed('auto_participant_rules', [{ status: 'active', requires_acceptance: false, accepted_at: '2026-01-01T00:00:00.000Z', effective_from: '2026-01-01T00:00:00.000Z', created_by: 'x', scope_type: 'all_personal_trips', ...extra }])[0] as unknown as AutoParticipantRule
}
const stored = (id: string) => db.table('auto_participant_rules').find((r) => r.id === id) as unknown as AutoParticipantRule
const row = (id: string) => db.table('trip_participants').find((r) => r.id === id) as unknown as TripParticipant
const status = async (g: Awaited<ReturnType<typeof requireCompanyAdminContext>>) => (g.ok ? 200 : g.response.status)
const body = async (g: Awaited<ReturnType<typeof requireCompanyAdminContext>>) => (g.ok ? null : await g.response.json())

beforeEach(() => {
  db.reset()
  mocks.getSessionUser.mockResolvedValue(MEMBER)
  mocks.loadActiveMode.mockResolvedValue(activeMode('X', 'member'))
})
afterEach(() => { vi.clearAllMocks(); vi.unstubAllEnvs() })

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

describe('Test J — requireCompanyAdminContext', () => {
  it('no session → 401; no company in the active mode → 403', async () => {
    mocks.getSessionUser.mockResolvedValue(null)
    expect(await status(await requireCompanyAdminContext())).toBe(401)
    mocks.getSessionUser.mockResolvedValue(MEMBER)
    mocks.loadActiveMode.mockResolvedValue(activeMode(null, 'company_admin'))
    const g = await requireCompanyAdminContext()
    expect(await status(g)).toBe(403)
    expect(await body(g)).toEqual({ error: 'Switch to a company mode first.' })
  })
  it('a member in company mode is refused even though they hold carrier_dispatcher there', async () => {
    company('X', 'CP Star')
    membership('m', 'X', 'member')
    const g = await requireCompanyAdminContext()
    expect(await status(g)).toBe(403)
    expect(await body(g)).toEqual({ error: 'Company Admin permission is required in this mode.' })
  })
  it('the mode claims company_admin but the membership row says member → 403 (re-read, §25)', async () => {
    company('X', 'CP Star')
    membership('m', 'X', 'member')
    mocks.loadActiveMode.mockResolvedValue(activeMode('X', 'company_admin'))
    const g = await requireCompanyAdminContext()
    expect(await status(g)).toBe(403)
    expect(await body(g)).toEqual({ error: 'Company Admin permission is required.' })
    // A pending company_admin membership does not count either.
    db.reset(); company('X', 'CP Star'); membership('m', 'X', 'company_admin', 'pending')
    expect(await status(await requireCompanyAdminContext())).toBe(403)
  })
  it('an approved company_admin membership in the active company → ok', async () => {
    company('X', 'CP Star')
    membership('m', 'X', 'company_admin')
    mocks.loadActiveMode.mockResolvedValue(activeMode('X', 'company_admin'))
    const g = await requireCompanyAdminContext()
    expect(g.ok).toBe(true)
    if (!g.ok) return
    expect(g).toMatchObject({ companyId: 'X', platformAdmin: false, company: { id: 'X', display_name: 'CP Star', locked: false }, user: { id: 'm' } })
    // The companyId option is ignored for a non-admin: the mode names the company.
    const other = await requireCompanyAdminContext({ companyId: 'Y' })
    expect(other.ok && other.companyId).toBe('X')
  })
  it('a platform admin names any company; a missing one is 404', async () => {
    company('X', 'CP Star')
    mocks.getSessionUser.mockResolvedValue(PLATFORM_ADMIN)
    mocks.loadActiveMode.mockResolvedValue(activeMode(null, 'member'))
    const g = await requireCompanyAdminContext({ companyId: 'X' })
    expect(g.ok && g.platformAdmin).toBe(true)
    expect(g.ok && g.companyId).toBe('X')
    const none = await requireCompanyAdminContext()
    expect(await status(none)).toBe(403)
    expect(await body(none)).toEqual({ error: 'Name a company.' })
    expect(await status(await requireCompanyAdminContext({ companyId: 'nope' }))).toBe(404)
  })
  it('a locked company answers 423 to its admin, not to a platform admin', async () => {
    company('X', 'CP Star', { locked: true })
    membership('m', 'X', 'company_admin')
    mocks.loadActiveMode.mockResolvedValue(activeMode('X', 'company_admin'))
    const g = await requireCompanyAdminContext()
    expect(await status(g)).toBe(423)
    mocks.getSessionUser.mockResolvedValue(PLATFORM_ADMIN)
    expect((await requireCompanyAdminContext({ companyId: 'X' })).ok).toBe(true)
  })
})

describe('Test M — POST /api/internal/auto-participants/evaluate', () => {
  const SECRET = 'cron-secret-for-tests-0123456789abcdefgh' // 40 chars
  const post = (payload: unknown, auth?: string) =>
    evaluate(new NextRequest('https://workspace.test/api/internal/auto-participants/evaluate', {
      method: 'POST', body: JSON.stringify(payload), headers: { 'content-type': 'application/json', ...(auth ? { authorization: auth } : {}) },
    }))

  it('a wrong, missing or unconfigured bearer → 401 and nothing runs', async () => {
    vi.stubEnv('CRON_SECRET', SECRET)
    const tripId = randomUUID(), participantId = randomUUID()
    expect((await post({ trip_id: tripId, participant_ids: [participantId] }, 'Bearer wrong')).status).toBe(401)
    expect((await post({ trip_id: tripId, participant_ids: [participantId] })).status).toBe(401)
    expect((await post({ trip_id: tripId, participant_ids: [participantId] }, `Bearer ${SECRET}x`)).status).toBe(401)
    vi.stubEnv('CRON_SECRET', 'short')
    expect((await post({ trip_id: tripId, participant_ids: [participantId] }, 'Bearer short')).status).toBe(401)
    expect(mocks.evaluateParticipantRulesById).not.toHaveBeenCalled()
  })
  it('a valid bearer runs the resolver per row with trigger synchron_import and no session', async () => {
    vi.stubEnv('CRON_SECRET', SECRET)
    const tripId = randomUUID(), p1 = randomUUID(), p2 = randomUUID()
    const res = await post({ trip_id: tripId, participant_ids: [p1, p2], trigger: 'synchron_import' }, `Bearer ${SECRET}`)
    expect(res.status).toBe(200)
    expect(await res.json()).toEqual({ ok: true, results: [
      { participant_id: p1, outcome: 'evaluated', added: ['new-row'], rules: [] },
      { participant_id: p2, outcome: 'evaluated', added: ['new-row'], rules: [] },
    ] })
    expect(mocks.evaluateParticipantRulesById).toHaveBeenCalledTimes(2)
    expect(mocks.evaluateParticipantRulesById).toHaveBeenNthCalledWith(1, { tripId, participantId: p1, trigger: 'synchron_import', session: null })
    // The default trigger is synchron_import; a resolver failure (null) is reported, not thrown.
    mocks.evaluateParticipantRulesById.mockResolvedValueOnce(null as never)
    const again = await post({ trip_id: tripId, participant_ids: [p1] }, `Bearer ${SECRET}`)
    expect((await again.json()).results[0]).toEqual({ participant_id: p1, outcome: 'failed', added: [], rules: [] })
    expect(mocks.evaluateParticipantRulesById).toHaveBeenLastCalledWith({ tripId, participantId: p1, trigger: 'synchron_import', session: null })
  })
  it('a malformed body → 400', async () => {
    vi.stubEnv('CRON_SECRET', SECRET)
    expect((await post({ trip_id: 'not-a-uuid', participant_ids: [randomUUID()] }, `Bearer ${SECRET}`)).status).toBe(400)
    expect((await post({ trip_id: randomUUID(), participant_ids: [] }, `Bearer ${SECRET}`)).status).toBe(400)
    expect((await post({}, `Bearer ${SECRET}`)).status).toBe(400)
  })
})

describe('Test T — self stop (D16)', () => {
  it('the participant revokes; a third party gets 403 and the rule is untouched', async () => {
    const a = person('a', 'Alex'), b = person('b', 'Bea'), c = person('c', 'Cal')
    const r = rule({ rule_type: 'personal', owner_user_id: a.id, participant_user_id: b.id, participant_email: b.email, participant_email_normalized: b.email, participant_role_type: 'carrier_dispatcher', requires_acceptance: true })
    expect(await respondToRule({ user: { ...c, role: 'dispatcher' }, ruleId: r.id, decision: 'stop' })).toMatchObject({ ok: false, status: 403 })
    expect(await respondToRule({ user: { ...a, role: 'dispatcher' }, ruleId: r.id, decision: 'stop' })).toMatchObject({ ok: false, status: 403 })
    expect(stored(r.id).status).toBe('active')
    expect(await respondToRule({ user: { ...b, role: 'dispatcher' }, ruleId: r.id, decision: 'stop' })).toMatchObject({ ok: true, rule: { status: 'revoked' } })
    expect(stored(r.id)).toMatchObject({ status: 'revoked', disabled_by: 'b' })
  })
})

describe('Test U — removing an auto-added participant (D17)', () => {
  function scene() {
    company('X', 'CP Star')
    const a = person('a', 'Alex'), b = person('b', 'Bea'), m = person('m', 'Mo Admin'), s = person('s', 'Sam Safety'), d = person('d', 'Dee Other')
    const t = trip()
    const personal = rule({ rule_type: 'personal', owner_user_id: a.id, participant_user_id: b.id, participant_email: b.email, participant_email_normalized: b.email, participant_role_type: 'carrier_dispatcher', requires_acceptance: true })
    const corporate = rule({ rule_type: 'company', owner_company_id: 'X', participant_user_id: s.id, participant_email: s.email, participant_email_normalized: s.email, participant_role_type: 'carrier_safety_manager', scope_type: 'carrier_trips' })
    participant(t.id as string, a)
    participant(t.id as string, m, { addition_method: 'manual_invite' })
    participant(t.id as string, d, { addition_method: 'manual_invite' })
    const rowB = participant(t.id as string, b, { addition_method: 'personal_auto_participant', triggering_user_id: a.id, invited_by: a.id, auto_participant_rule_id: personal.id })
    const rowS = participant(t.id as string, s, { addition_method: 'company_auto_participant', triggering_user_id: m.id, triggering_company_id: 'X', invited_by: m.id, auto_participant_rule_id: corporate.id, context_company_id: 'X', context_role_type: 'carrier_safety_manager' })
    return { t, a, b, m, s, d, personal, corporate, rowB, rowS }
  }
  const removedEvents = () => mocks.logTripEvent.mock.calls.map((c) => (c as unknown as [{ action: string; detail: Record<string, unknown>; actorId: string | null }])[0]).filter((c) => c.action === 'participant_removed')

  it('the triggering user removes the personal auto-added row; the rule stays active', async () => {
    const { t, rowB, personal } = scene()
    const res = await removeParticipantFromTrip({ tripId: t.id as string, participantId: rowB.id, viewer: { userId: 'a', role: 'dispatcher', isAdmin: false, label: 'Alex' } })
    expect(res).toMatchObject({ ok: true, participant: { id: rowB.id, status: 'removed' } })
    expect(row(rowB.id).status).toBe('removed')
    expect(removedEvents()).toHaveLength(1)
    expect(removedEvents()[0]).toMatchObject({ actorId: 'a', detail: { participant_id: rowB.id, email: 'b@users.test', role: 'dispatcher', addition_method: 'personal_auto_participant', auto_participant_rule_id: personal.id } })
    expect(db.table('auto_participant_rule_events').filter((e) => e.event === 'participant_removed_from_trip' && e.rule_id === personal.id)).toHaveLength(1)
    expect(stored(personal.id).status).toBe('active')
    // Already removed → 409.
    expect(await removeParticipantFromTrip({ tripId: t.id as string, participantId: rowB.id, viewer: { userId: 'a', role: 'dispatcher', isAdmin: false, label: 'Alex' } })).toMatchObject({ ok: false, status: 409 })
  })
  it('a Company Admin acting in X removes X’s company auto-added row; outside X they cannot', async () => {
    const { t, rowS, corporate } = scene()
    const outside = await removeParticipantFromTrip({ tripId: t.id as string, participantId: rowS.id, viewer: { userId: 'm', role: 'dispatcher', isAdmin: false, label: 'Mo', activeCompanyId: 'Y', activeIsCompanyAdmin: true } })
    expect(outside).toMatchObject({ ok: false, status: 403 })
    const notAdmin = await removeParticipantFromTrip({ tripId: t.id as string, participantId: rowS.id, viewer: { userId: 'm', role: 'dispatcher', isAdmin: false, label: 'Mo', activeCompanyId: 'X', activeIsCompanyAdmin: false } })
    expect(notAdmin).toMatchObject({ ok: false, status: 403 })
    expect(row(rowS.id).status).toBe('active')

    const res = await removeParticipantFromTrip({ tripId: t.id as string, participantId: rowS.id, viewer: { userId: 'm', role: 'dispatcher', isAdmin: false, label: 'Mo', activeCompanyId: 'X', activeIsCompanyAdmin: true } })
    expect(res.ok).toBe(true)
    expect(row(rowS.id).status).toBe('removed')
    expect(removedEvents()[0].detail).toMatchObject({ participant_id: rowS.id, addition_method: 'company_auto_participant', auto_participant_rule_id: corporate.id })
    expect(stored(corporate.id).status).toBe('active')
  })
  it('a dispatcher who is neither cannot remove a dispatcher-role row; a platform admin can', async () => {
    const { t, rowB, rowS, personal, corporate } = scene()
    for (const id of [rowB.id, rowS.id]) {
      const res = await removeParticipantFromTrip({ tripId: t.id as string, participantId: id, viewer: { userId: 'd', role: 'dispatcher', isAdmin: false, label: 'Dee' } })
      expect(res).toEqual({ ok: false, error: 'You cannot remove this participant.', status: 403 })
      expect(row(id).status).toBe('active')
    }
    expect(removedEvents()).toHaveLength(0)
    expect(await removeParticipantFromTrip({ tripId: t.id as string, participantId: rowB.id, viewer: { userId: 'adm', role: 'admin', isAdmin: true, label: 'Ada' } })).toMatchObject({ ok: true })
    expect(await removeParticipantFromTrip({ tripId: 'other-trip', participantId: rowS.id, viewer: { userId: 'adm', role: 'admin', isAdmin: true, label: 'Ada' } })).toMatchObject({ ok: false, status: 404 })
    expect(stored(personal.id).status).toBe('active')
    expect(stored(corporate.id).status).toBe('active')
  })
})
