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

/**
 * Auto-Participant rule management (docs/TASKS-2026-09-30-AUTO-PARTICIPANTS.md
 * §6, §7, §13 "Accept flow"): createPersonalRule, respondToRule,
 * createCompanyRule and ruleOperation against 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 { createCompanyRule, createPersonalRule, listRulesForUser, respondToRule, ruleOperation } = await import('@/lib/data/auto-participant-rules')

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

type EmailCall = { templateKey: string; to: string; userId: string | null; data: Record<string, string> }
const emails = () => mocks.sendPlatformEmail.mock.calls.map((c) => (c as unknown as [EmailCall])[0])

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) {
  return db.seed('companies', [{ id, display_name: name, legal_name: `${name} LLC`, company_type: 'carrier', locked: false }])[0]
}
function member(userId: string, companyId: string, roles: string[]) {
  const [m] = db.seed('company_memberships', [{ user_id: userId, company_id: companyId, status: 'approved', permission_level: 'member', 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
}
const stored = (id: string) => db.table('auto_participant_rules').find((r) => r.id === id) as unknown as AutoParticipantRule
const asUser = (p: { id: string; email: string; name: string }, role = 'dispatcher') => ({ ...p, role })
const ACTOR = { id: 'adm', email: 'adm@users.test', name: 'Ada Admin' }

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

/* ------------------------------------------------------------ personal */

describe('createPersonalRule', () => {
  it('refuses yourself and malformed addresses', async () => {
    const a = person('a', 'Alex')
    expect(await createPersonalRule({ owner: a, email: 'A@Users.test' })).toMatchObject({ ok: false, status: 400 })
    expect(await createPersonalRule({ owner: a, email: 'not-an-email' })).toMatchObject({ ok: false, status: 400 })
    expect(db.table('auto_participant_rules')).toHaveLength(0)
  })
  it('an unknown email needs first and last name (422) and is then held pending with a request email (D4, D5)', async () => {
    const a = person('a', 'Alex')
    expect(await createPersonalRule({ owner: a, email: 'maria@users.test', first_name: 'Maria' })).toMatchObject({ ok: false, status: 422 })
    expect(mocks.sendPlatformEmail).not.toHaveBeenCalled()

    const res = await createPersonalRule({ owner: a, email: 'Maria@users.test', first_name: 'Maria', last_name: 'Lopez', origin: 'https://app.test' })
    expect(res.ok).toBe(true)
    if (!res.ok) return
    expect(res.rule).toMatchObject({
      rule_type: 'personal', owner_user_id: 'a', participant_user_id: null, participant_email_normalized: 'maria@users.test',
      participant_first_name: 'Maria', participant_last_name: 'Lopez', participant_role_type: null, scope_type: 'all_personal_trips', status: 'pending', requires_acceptance: true,
    })
    expect(res.rule.accept_token).toEqual(expect.any(String))
    expect(emails()).toHaveLength(1)
    expect(emails()[0]).toMatchObject({ templateKey: 'auto_participant_request', to: 'Maria@users.test', userId: null })
    expect(emails()[0].data).toMatchObject({ first_name: 'Maria', owner_name: 'Alex', accept_link: `https://app.test/auto-participants/${res.rule.accept_token}` })
    expect(db.table('auto_participant_rule_events').find((e) => e.event === 'rule_requested')?.detail).toMatchObject({ participant_email: 'maria@users.test', has_account: false })
  })
  it('an unclaimed historical profile is not an account yet (§31)', async () => {
    const a = person('a', 'Alex')
    db.seed('profiles', [{ id: 'h', email: 'hist@users.test', full_name: 'Historic', claim_status: 'unclaimed' }])
    expect(await createPersonalRule({ owner: a, email: 'hist@users.test' })).toMatchObject({ ok: false, status: 422 })
    const res = await createPersonalRule({ owner: a, email: 'hist@users.test', first_name: 'His', last_name: 'Toric' })
    expect(res.ok && res.rule.participant_user_id).toBeNull()
  })
  it('an existing account resolves at once and is still pending acceptance; a second request is a 409', async () => {
    const a = person('a', 'Alex'), b = person('b', 'Bea')
    const res = await createPersonalRule({ owner: a, email: 'B@users.test' })
    expect(res.ok).toBe(true)
    if (!res.ok) return
    expect(res.rule).toMatchObject({ participant_user_id: 'b', participant_email: 'b@users.test', status: 'pending', participant_first_name: null })
    expect(emails()[0]).toMatchObject({ templateKey: 'auto_participant_request', to: 'b@users.test', userId: 'b' })
    expect(emails()[0].data.first_name).toBe('Bea')

    expect(await createPersonalRule({ owner: a, email: 'b@users.test' })).toMatchObject({ ok: false, status: 409 })
    expect(db.table('auto_participant_rules')).toHaveLength(1)
    expect(mocks.sendPlatformEmail).toHaveBeenCalledTimes(1)
  })
})

describe('respondToRule', () => {
  async function pendingRule(owner: { id: string; email: string; name: string }, email: string) {
    const res = await createPersonalRule({ owner, email })
    if (!res.ok) throw new Error(res.error)
    return res.rule
  }

  it('only the person the rule names may answer (403)', async () => {
    const a = person('a', 'Alex'), b = person('b', 'Bea'), c = person('c', 'Cal')
    const rule = await pendingRule(a, b.email)
    expect(await respondToRule({ user: asUser(c), ruleId: rule.id, decision: 'accept' })).toMatchObject({ ok: false, status: 403 })
    expect(await respondToRule({ user: asUser(c), token: rule.accept_token!, decision: 'decline' })).toMatchObject({ ok: false, status: 403 })
    expect(stored(rule.id).status).toBe('pending')
    expect(await respondToRule({ user: asUser(c), ruleId: 'missing', decision: 'accept' })).toMatchObject({ ok: false, status: 404 })
  })
  it('decline', async () => {
    const a = person('a', 'Alex'), b = person('b', 'Bea')
    const rule = await pendingRule(a, b.email)
    const res = await respondToRule({ user: asUser(b), token: rule.accept_token!, decision: 'decline' })
    expect(res).toMatchObject({ ok: true, rule: { status: 'declined' } })
    expect(stored(rule.id).status).toBe('declined')
    expect(db.table('auto_participant_rule_events').some((e) => e.event === 'rule_declined' && e.actor_user_id === 'b')).toBe(true)
    // Nothing further can happen to it.
    expect(await respondToRule({ user: asUser(b), ruleId: rule.id, decision: 'accept' })).toMatchObject({ ok: false, status: 409 })
  })
  it('accept with one role → active with that role (D1, D6)', async () => {
    const a = person('a', 'Alex'), b = person('b', 'Bea') // account role dispatcher → carrier_dispatcher only
    const rule = await pendingRule(a, b.email)
    const res = await respondToRule({ user: asUser(b), ruleId: rule.id, decision: 'accept' })
    expect(res.ok).toBe(true)
    expect(stored(rule.id)).toMatchObject({ status: 'active', participant_user_id: 'b', participant_role_type: 'carrier_dispatcher', accepted_by: 'b', accepted_at: expect.any(String), effective_from: expect.any(String) })
    expect(db.table('auto_participant_rule_events').find((e) => e.event === 'rule_accepted')?.detail).toMatchObject({ role_type: 'carrier_dispatcher' })
  })
  it('accept with two roles → 400 until role_type names one of them', async () => {
    company('X', 'CP Star')
    const a = person('a', 'Alex'), d = person('d', 'Dee Dual', { default_role: 'broker' })
    member('d', 'X', ['carrier_dispatcher']) // + freight_broker from the account itself
    const rule = await pendingRule(a, d.email)

    const noRole = await respondToRule({ user: asUser(d, 'broker'), ruleId: rule.id, decision: 'accept' })
    expect(noRole).toMatchObject({ ok: false, status: 400, needs_role: ['carrier_dispatcher', 'freight_broker'] })
    expect(stored(rule.id).status).toBe('pending')
    const wrong = await respondToRule({ user: asUser(d, 'broker'), ruleId: rule.id, decision: 'accept', role_type: 'pilot_driver' })
    expect(wrong).toMatchObject({ ok: false, status: 400 })

    const res = await respondToRule({ user: asUser(d, 'broker'), ruleId: rule.id, decision: 'accept', role_type: 'freight_broker' })
    expect(res).toMatchObject({ ok: true, rule: { status: 'active', participant_role_type: 'freight_broker' } })
  })
  it('a person with no role yet cannot accept (409)', async () => {
    const a = person('a', 'Alex'), n = person('n', 'Nobody', { default_role: 'admin' })
    const rule = await pendingRule(a, n.email)
    expect(await respondToRule({ user: asUser(n, 'admin'), ruleId: rule.id, decision: 'accept' })).toMatchObject({ ok: false, status: 409 })
  })
  it('stop → revoked for an active rule; a pending one cannot be stopped (D16)', async () => {
    const a = person('a', 'Alex'), b = person('b', 'Bea')
    const rule = await pendingRule(a, b.email)
    expect(await respondToRule({ user: asUser(b), ruleId: rule.id, decision: 'stop' })).toMatchObject({ ok: false, status: 409 })
    await respondToRule({ user: asUser(b), ruleId: rule.id, decision: 'accept' })
    const res = await respondToRule({ user: asUser(b), ruleId: rule.id, decision: 'stop' })
    expect(res).toMatchObject({ ok: true, rule: { status: 'revoked' } })
    expect(stored(rule.id)).toMatchObject({ status: 'revoked', disabled_by: 'b', disabled_at: expect.any(String) })
    expect(db.table('auto_participant_rule_events').some((e) => e.event === 'rule_stopped_by_participant')).toBe(true)
    // The owner's list no longer shows it; the person's list never did.
    const mine = await listRulesForUser(a)
    expect(mine.mine).toEqual([])
    const theirs = await listRulesForUser(b)
    expect(theirs.including_me).toEqual([])
    expect(theirs.pending_for_me).toEqual([])
  })
})

/* ------------------------------------------------------------- company */

describe('createCompanyRule', () => {
  const base = { actor: ACTOR, companyId: 'X', companyName: 'CP Star', scope_type: 'carrier_trips' }

  it('a member with one role is active at once with that role, no email', async () => {
    company('X', 'CP Star')
    const s = person('s', 'Sam Safety')
    member('s', 'X', ['carrier_safety_manager'])
    const res = await createCompanyRule({ ...base, member_user_id: 's' })
    expect(res).toMatchObject({ ok: true, external: false })
    if (!res.ok) return
    expect(res.rule).toMatchObject({
      rule_type: 'company', owner_company_id: 'X', owner_user_id: null, participant_user_id: 's', participant_email_normalized: 's@users.test',
      participant_role_type: 'carrier_safety_manager', scope_type: 'carrier_trips', status: 'active', requires_acceptance: false,
      accepted_by: 'adm', accepted_at: expect.any(String), effective_from: expect.any(String), created_by: 'adm',
    })
    expect(mocks.sendPlatformEmail).not.toHaveBeenCalled()
    expect(db.table('auto_participant_rule_events').find((e) => e.event === 'rule_created')?.detail).toMatchObject({ company_id: 'X', scope: 'carrier_trips', external: false, authorized_by: 'Ada Admin' })
    // Same person, same scope again → 409; another scope is fine.
    expect(await createCompanyRule({ ...base, member_user_id: 's' })).toMatchObject({ ok: false, status: 409 })
    expect((await createCompanyRule({ ...base, member_user_id: 's', scope_type: 'all_company_trips' })).ok).toBe(true)
  })
  it('a member with two roles is active with no role — they pick per trip (A4)', async () => {
    company('X', 'CP Star')
    person('f', 'Fay Fleet')
    member('f', 'X', ['carrier_fleet_manager', 'carrier_driver'])
    const res = await createCompanyRule({ ...base, member_user_id: 'f' })
    expect(res.ok && res.rule).toMatchObject({ status: 'active', participant_role_type: null })
  })
  it('a member without a role at the company → 409', async () => {
    company('X', 'CP Star')
    person('n', 'No Role')
    member('n', 'X', [])
    expect(await createCompanyRule({ ...base, member_user_id: 'n' })).toMatchObject({ ok: false, status: 409 })
    expect(await createCompanyRule({ ...base, member_user_id: 'ghost' })).toMatchObject({ ok: false, status: 404 })
  })
  it('an outsider needs confirm_external (412), then becomes a pending request with the company email', async () => {
    company('X', 'CP Star')
    const e = person('e', 'Eve External')
    expect(await createCompanyRule({ ...base, email: e.email })).toMatchObject({ ok: false, status: 412 })
    expect(db.table('auto_participant_rules')).toHaveLength(0)

    const res = await createCompanyRule({ ...base, email: 'E@users.test', confirm_external: true, origin: 'https://app.test' })
    expect(res).toMatchObject({ ok: true, external: true })
    if (!res.ok) return
    expect(res.rule).toMatchObject({ participant_user_id: 'e', status: 'pending', requires_acceptance: true, accepted_at: null, effective_from: null, participant_role_type: null })
    expect(emails()).toHaveLength(1)
    expect(emails()[0]).toMatchObject({ templateKey: 'auto_participant_company_request', to: 'e@users.test', userId: 'e' })
    expect(emails()[0].data).toMatchObject({ first_name: 'Eve', company_name: 'CP Star', scope_label: 'Carrier Trips', requested_by: 'Ada Admin', accept_link: `https://app.test/auto-participants/${res.rule.accept_token}` })
    expect(db.table('auto_participant_rule_events').find((e) => e.event === 'rule_requested')?.detail).toMatchObject({ external: true })
  })
  it('an unknown outsider needs names (422); bad input is refused', async () => {
    company('X', 'CP Star')
    expect(await createCompanyRule({ ...base, email: 'new@users.test', confirm_external: true })).toMatchObject({ ok: false, status: 422 })
    const res = await createCompanyRule({ ...base, email: 'new@users.test', first_name: 'New', last_name: 'Person', confirm_external: true })
    expect(res.ok && res.rule).toMatchObject({ participant_user_id: null, participant_first_name: 'New', status: 'pending' })
    expect(await createCompanyRule({ ...base, scope_type: 'all_personal_trips', email: 'x@users.test' })).toMatchObject({ ok: false, status: 400 })
    expect(await createCompanyRule({ ...base })).toMatchObject({ ok: false, status: 400 })
  })
})

/* ---------------------------------------------------------------- ops */

describe('ruleOperation', () => {
  async function activeCompanyRule() {
    company('X', 'CP Star')
    person('s', 'Sam Safety')
    member('s', 'X', ['carrier_safety_manager'])
    const res = await createCompanyRule({ actor: ACTOR, companyId: 'X', companyName: 'CP Star', scope_type: 'carrier_trips', member_user_id: 's' })
    if (!res.ok) throw new Error(res.error)
    return res.rule
  }
  async function pendingPersonalRule() {
    const a = person('a', 'Alex'), b = person('b', 'Bea')
    const res = await createPersonalRule({ owner: a, email: b.email })
    if (!res.ok) throw new Error(res.error)
    return res.rule
  }
  const op = (rule: AutoParticipantRule, o: Parameters<typeof ruleOperation>[0]['op'], extra: Partial<Parameters<typeof ruleOperation>[0]> = {}) =>
    ruleOperation({ actor: ACTOR, rule: stored(rule.id), op: o, ...extra })

  it('disable ↔ reactivate', async () => {
    const rule = await activeCompanyRule()
    expect(await op(rule, 'reactivate')).toMatchObject({ ok: false, status: 409 })
    expect(await op(rule, 'disable')).toMatchObject({ ok: true, rule: { status: 'disabled', disabled_by: 'adm' } })
    expect(stored(rule.id).disabled_at).toEqual(expect.any(String))
    expect(await op(rule, 'disable')).toMatchObject({ ok: false, status: 409 })
    expect(await op(rule, 'reactivate')).toMatchObject({ ok: true, rule: { status: 'active', disabled_at: null, disabled_by: null } })
    expect(db.table('auto_participant_rule_events').map((e) => e.event)).toEqual(expect.arrayContaining(['rule_disabled', 'rule_reactivated']))
  })
  it('a pending request cannot be disabled or reactivated, only cancelled or resent', async () => {
    const rule = await pendingPersonalRule()
    expect(await op(rule, 'disable')).toMatchObject({ ok: false, status: 409 })
    expect(await op(rule, 'reactivate')).toMatchObject({ ok: false, status: 409 })
    const before = stored(rule.id).accept_token
    const resent = await op(rule, 'resend', { ownerName: 'Alex' })
    expect(resent.ok).toBe(true)
    expect(stored(rule.id).accept_token).not.toBe(before)
    expect(emails().map((e) => e.templateKey)).toEqual(['auto_participant_request', 'auto_participant_request'])
    expect(emails()[1].data.accept_link).toContain(stored(rule.id).accept_token!)
    expect(await op(rule, 'cancel')).toMatchObject({ ok: true, rule: { status: 'revoked' } })
    expect(await op(rule, 'resend')).toMatchObject({ ok: false, status: 409 })
    expect(db.table('auto_participant_rule_events').some((e) => e.event === 'rule_cancelled')).toBe(true)
  })
  it('remove is final; resend needs a pending rule', async () => {
    const rule = await activeCompanyRule()
    expect(await op(rule, 'resend')).toMatchObject({ ok: false, status: 409 })
    expect(await op(rule, 'remove')).toMatchObject({ ok: true, rule: { status: 'revoked', disabled_by: 'adm' } })
    expect(await op(rule, 'remove')).toMatchObject({ ok: false, status: 409 })
    expect(await op(rule, 'reactivate')).toMatchObject({ ok: false, status: 409 })
    expect(await op(rule, 'disable')).toMatchObject({ ok: false, status: 409 })
    expect(db.table('auto_participant_rule_events').some((e) => e.event === 'rule_removed')).toBe(true)
  })
  it('a suspended company rule comes back only with an approved membership (Task 10.1)', async () => {
    const rule = await activeCompanyRule()
    await db.from('auto_participant_rules').update({ status: 'suspended', suspended_reason: 'membership_revoked' }).eq('id', rule.id)
    await db.from('company_memberships').update({ status: 'revoked' }).eq('user_id', 's')
    expect(await op(rule, 'reactivate')).toMatchObject({ ok: false, status: 409 })
    await db.from('company_memberships').update({ status: 'approved' }).eq('user_id', 's')
    expect(await op(rule, 'reactivate')).toMatchObject({ ok: true, rule: { status: 'active', suspended_reason: null } })
  })
  it('change_scope: company scopes for company rules, nothing else; only live rules', async () => {
    const rule = await activeCompanyRule()
    expect(await op(rule, 'change_scope', { scope_type: 'all_personal_trips' })).toMatchObject({ ok: false, status: 400 })
    expect(await op(rule, 'change_scope')).toMatchObject({ ok: false, status: 400 })
    expect(await op(rule, 'change_scope', { scope_type: 'all_company_trips' })).toMatchObject({ ok: true, rule: { scope_type: 'all_company_trips' } })
    expect(db.table('auto_participant_rule_events').find((e) => e.event === 'rule_scope_changed')?.detail).toEqual({ from: 'carrier_trips', to: 'all_company_trips' })
    // A second live rule for the same person already has carrier_trips → the unique index answers 409.
    const second = await createCompanyRule({ actor: ACTOR, companyId: 'X', companyName: 'CP Star', scope_type: 'carrier_trips', member_user_id: 's' })
    expect(second.ok).toBe(true)
    expect(await op(rule, 'change_scope', { scope_type: 'carrier_trips' })).toMatchObject({ ok: false, status: 409 })
    await op(rule, 'remove')
    expect(await op(rule, 'change_scope', { scope_type: 'pilot_company_trips' })).toMatchObject({ ok: false, status: 409 })

    const personal = await pendingPersonalRule()
    expect(await op(personal, 'change_scope', { scope_type: 'carrier_trips' })).toMatchObject({ ok: false, status: 400 })
    expect(await op(personal, 'change_scope', { scope_type: 'all_personal_trips' })).toMatchObject({ ok: true })
  })
  it('unknown operation → 400', async () => {
    const rule = await activeCompanyRule()
    expect(await ruleOperation({ actor: ACTOR, rule, op: 'explode' as never })).toMatchObject({ ok: false, status: 400 })
  })
})
