import { describe, expect, it } from 'vitest'
import {
  RULE_TRANSITIONS, autoAddDecision, canTransition, companyScopeMatches, contextChoice, initialTripRoleForCompanyMember,
  provenanceLine, roleTypesMatchingTripRole, tripRoleForRoleType, type AutoAddInput,
} from '@/lib/domain/auto-participants'
import { canRemoveParticipant } from '@/lib/domain/participants'
import { ROLE_TYPES, type RoleType } from '@/lib/domain/modes'
import type { AutoParticipantRuleStatus, AutoParticipantScope } from '@/types/db'

/**
 * Pure Auto-Participant rules (docs/TASKS-2026-09-30-AUTO-PARTICIPANTS.md §4,
 * §13 "Pure domain"). No I/O.
 */

describe('rule status transitions (Task 4.1)', () => {
  it('matches the table in the spec', () => {
    expect(RULE_TRANSITIONS).toEqual({
      pending: ['active', 'declined', 'revoked'],
      active: ['disabled', 'revoked', 'suspended'],
      disabled: ['active', 'revoked'],
      suspended: ['active', 'revoked'],
      declined: [],
      revoked: [],
    })
  })
  it('canTransition follows the table and refuses everything else', () => {
    const all: AutoParticipantRuleStatus[] = ['pending', 'active', 'declined', 'revoked', 'disabled', 'suspended']
    for (const from of all) for (const to of all) expect(canTransition(from, to)).toBe(RULE_TRANSITIONS[from].includes(to))
    expect(canTransition('pending', 'active')).toBe(true)
    expect(canTransition('active', 'active')).toBe(false)
    expect(canTransition('declined', 'active')).toBe(false)
    expect(canTransition('revoked', 'pending')).toBe(false)
  })
})

describe('trip role derivation (Task 4.2, D1)', () => {
  it('maps all nine role types', () => {
    expect(tripRoleForRoleType('carrier_dispatcher')).toBe('dispatcher')
    expect(tripRoleForRoleType('carrier_safety_manager')).toBe('dispatcher')
    expect(tripRoleForRoleType('carrier_permit_manager')).toBe('dispatcher')
    expect(tripRoleForRoleType('carrier_accounting')).toBe('dispatcher')
    expect(tripRoleForRoleType('carrier_fleet_manager')).toBe('dispatcher')
    expect(tripRoleForRoleType('carrier_driver')).toBe('driver')
    expect(tripRoleForRoleType('freight_broker')).toBe('broker')
    expect(tripRoleForRoleType('pilot_company_dispatch')).toBe('pilot')
    expect(tripRoleForRoleType('pilot_driver')).toBe('pilot')
    expect(tripRoleForRoleType(null)).toBeNull()
    expect(tripRoleForRoleType('dependent')).toBeNull()
    // Every role type resolves to a trip role — none is left out.
    for (const r of ROLE_TYPES) expect(tripRoleForRoleType(r)).not.toBeNull()
  })
  it('roleTypesMatchingTripRole is the reverse set', () => {
    expect(roleTypesMatchingTripRole('dispatcher')).toEqual(['carrier_dispatcher', 'carrier_safety_manager', 'carrier_permit_manager', 'carrier_accounting', 'carrier_fleet_manager'])
    expect(roleTypesMatchingTripRole('driver')).toEqual(['carrier_driver'])
    expect(roleTypesMatchingTripRole('broker')).toEqual(['freight_broker'])
    expect(roleTypesMatchingTripRole('pilot')).toEqual(['pilot_company_dispatch', 'pilot_driver'])
    expect(roleTypesMatchingTripRole('shipper')).toEqual([])
    expect(roleTypesMatchingTripRole('admin')).toEqual([])
  })
})

describe('company scope matching (Task 4.3)', () => {
  const carrierOffice: RoleType[] = ['carrier_dispatcher', 'carrier_safety_manager', 'carrier_permit_manager', 'carrier_accounting', 'carrier_fleet_manager']
  const expected: Record<AutoParticipantScope, (r: RoleType) => boolean> = {
    all_company_trips: () => true,
    carrier_trips: (r) => r === 'carrier_driver' || carrierOffice.includes(r),
    freight_broker_trips: (r) => r === 'freight_broker',
    pilot_company_trips: (r) => r === 'pilot_company_dispatch' || r === 'pilot_driver',
    all_personal_trips: () => false,
  }
  it('every scope × every role family', () => {
    for (const scope of Object.keys(expected) as AutoParticipantScope[]) {
      for (const role of ROLE_TYPES) expect(companyScopeMatches(scope, role), `${scope} × ${role}`).toBe(expected[scope](role))
    }
  })
  it('an unknown or missing context role never matches', () => {
    expect(companyScopeMatches('all_company_trips', null)).toBe(false)
    expect(companyScopeMatches('all_company_trips', undefined)).toBe(false)
    expect(companyScopeMatches('all_company_trips', 'dependent')).toBe(false)
    expect(companyScopeMatches('carrier_trips', 'freight_broker')).toBe(false)
  })
})

describe('autoAddDecision (Task 4.4)', () => {
  const base = (): AutoAddInput => ({
    rule: { status: 'active', effective_from: '2026-01-01T00:00:00.000Z', participant_user_id: 'B', rule_type: 'personal' },
    trigger: { participant: { user_id: 'A', addition_method: 'trip_creator' }, kind: 'participation', at: '2026-09-30T12:00:00.000Z', session: { id: 'A', originId: 'A', preview: null } },
    participant: { userId: 'B', blockedAt: null, claimStatus: null, hasAccount: true, membershipOk: true, existingStatus: null },
  })
  const withRule = (patch: Partial<AutoAddInput['rule']>) => { const i = base(); Object.assign(i.rule, patch); return i }
  const withParticipant = (patch: Partial<AutoAddInput['participant']>) => { const i = base(); Object.assign(i.participant, patch); return i }

  it('add', () => expect(autoAddDecision(base())).toEqual({ add: true }))
  it('rule_not_active', () => {
    for (const status of ['pending', 'disabled', 'suspended', 'declined', 'revoked'] as const) expect(autoAddDecision(withRule({ status }))).toEqual({ add: false, reason: 'rule_not_active' })
  })
  it('recursive — an auto-added trigger never triggers', () => {
    const i = base(); i.trigger.participant = { user_id: 'A', addition_method: 'personal_auto_participant' }
    expect(autoAddDecision(i)).toEqual({ add: false, reason: 'recursive' })
    i.trigger.participant = { user_id: 'A', addition_method: 'company_auto_participant' }
    expect(autoAddDecision(i)).toEqual({ add: false, reason: 'recursive' })
  })
  it('admin_preview — a switched or previewing session', () => {
    const i = base(); i.trigger.session = { id: 'A', originId: 'admin', preview: null }
    expect(autoAddDecision(i)).toEqual({ add: false, reason: 'admin_preview' })
    i.trigger.session = { id: 'A', originId: 'A', preview: 'carrier' }
    expect(autoAddDecision(i)).toEqual({ add: false, reason: 'admin_preview' })
    i.trigger.session = null
    expect(autoAddDecision(i)).toEqual({ add: true })
  })
  it('before_effective_from', () => {
    expect(autoAddDecision(withRule({ effective_from: '2026-10-01T00:00:00.000Z' }))).toEqual({ add: false, reason: 'before_effective_from' })
    expect(autoAddDecision(withRule({ effective_from: null }))).toEqual({ add: true })
  })
  it('participant_unresolved — no user on either side', () => {
    expect(autoAddDecision(withParticipant({ userId: null }))).toEqual({ add: false, reason: 'participant_unresolved' })
    expect(autoAddDecision(withRule({ participant_user_id: null }))).toEqual({ add: false, reason: 'participant_unresolved' })
  })
  it('self', () => expect(autoAddDecision(withParticipant({ userId: 'A' }))).toEqual({ add: false, reason: 'self' }))
  it('participant_blocked', () => expect(autoAddDecision(withParticipant({ blockedAt: '2026-09-01T00:00:00.000Z' }))).toEqual({ add: false, reason: 'participant_blocked' }))
  it('participant_not_claimed', () => {
    for (const claimStatus of ['unclaimed', 'claim_candidate', 'manual_review', 'conflict']) expect(autoAddDecision(withParticipant({ claimStatus }))).toEqual({ add: false, reason: 'participant_not_claimed' })
    expect(autoAddDecision(withParticipant({ hasAccount: false }))).toEqual({ add: false, reason: 'participant_not_claimed' })
    expect(autoAddDecision(withParticipant({ claimStatus: 'claimed' }))).toEqual({ add: true })
  })
  it('membership_lost — company rules only', () => {
    const i = withParticipant({ membershipOk: false }); i.rule.rule_type = 'company'
    expect(autoAddDecision(i)).toEqual({ add: false, reason: 'membership_lost' })
    expect(autoAddDecision(withParticipant({ membershipOk: false }))).toEqual({ add: true })
  })
  it('already_on_trip / explicitly_removed / imported_row_exists', () => {
    expect(autoAddDecision(withParticipant({ existingStatus: 'invited' }))).toEqual({ add: false, reason: 'already_on_trip' })
    expect(autoAddDecision(withParticipant({ existingStatus: 'active' }))).toEqual({ add: false, reason: 'already_on_trip' })
    expect(autoAddDecision(withParticipant({ existingStatus: 'removed' }))).toEqual({ add: false, reason: 'explicitly_removed' })
    expect(autoAddDecision(withParticipant({ existingStatus: 'imported' }))).toEqual({ add: false, reason: 'imported_row_exists' })
  })
  it('gates apply in order: an inactive rule wins over a blocked person', () => {
    const i = withParticipant({ blockedAt: '2026-09-01T00:00:00.000Z' }); i.rule.status = 'disabled'
    expect(autoAddDecision(i)).toEqual({ add: false, reason: 'rule_not_active' })
  })
})

describe('contextChoice (D13)', () => {
  const ctx = (roleType: RoleType, companyId: string | null, status = 'active') => ({ roleType, companyId, status })
  it('one matching context is chosen silently', () => {
    const only = ctx('carrier_dispatcher', 'X')
    expect(contextChoice([only, ctx('freight_broker', 'Y')], 'dispatcher')).toEqual({ kind: 'one', context: only })
  })
  it('several matching contexts → choose, none → none', () => {
    const a = ctx('carrier_dispatcher', 'X'), b = ctx('carrier_safety_manager', 'Z')
    expect(contextChoice([a, b], 'dispatcher')).toEqual({ kind: 'choose', contexts: [a, b] })
    expect(contextChoice([ctx('freight_broker', 'Y')], 'dispatcher')).toEqual({ kind: 'none' })
    expect(contextChoice([], 'dispatcher')).toEqual({ kind: 'none' })
  })
  it('only active contexts count', () => {
    expect(contextChoice([ctx('carrier_dispatcher', 'X', 'pending'), ctx('carrier_dispatcher', 'Z', 'revoked')], 'dispatcher')).toEqual({ kind: 'none' })
  })
  it('a company restriction picks among the roles held THERE regardless of trip role (A4)', () => {
    const atX = ctx('carrier_driver', 'X'), elsewhere = ctx('carrier_dispatcher', 'Z')
    expect(contextChoice([atX, elsewhere], 'dispatcher', 'X')).toEqual({ kind: 'one', context: atX })
    const alsoX = ctx('carrier_fleet_manager', 'X')
    expect(contextChoice([atX, alsoX, elsewhere], 'dispatcher', 'X')).toEqual({ kind: 'choose', contexts: [atX, alsoX] })
    expect(contextChoice([elsewhere], 'dispatcher', 'X')).toEqual({ kind: 'none' })
  })
})

describe('initialTripRoleForCompanyMember (A11)', () => {
  it('dispatcher-power role first, otherwise the first mapped role, null for none', () => {
    expect(initialTripRoleForCompanyMember(['carrier_driver', 'carrier_fleet_manager'])).toBe('dispatcher')
    expect(initialTripRoleForCompanyMember(['carrier_driver'])).toBe('driver')
    expect(initialTripRoleForCompanyMember(['pilot_driver', 'pilot_company_dispatch'])).toBe('pilot')
    expect(initialTripRoleForCompanyMember(['freight_broker'])).toBe('broker')
    expect(initialTripRoleForCompanyMember(['unknown_role'])).toBeNull()
    expect(initialTripRoleForCompanyMember([])).toBeNull()
  })
})

describe('provenanceLine (§26)', () => {
  it('names the triggering user for personal, the company for company rules, nothing otherwise', () => {
    expect(provenanceLine({ addition_method: 'personal_auto_participant', triggering_user_name: 'Alex Carrier' })).toBe('Auto-added via Alex Carrier')
    expect(provenanceLine({ addition_method: 'personal_auto_participant', triggering_user_name: null })).toBe('Auto-added via an Auto-Participant rule')
    expect(provenanceLine({ addition_method: 'company_auto_participant', triggering_company_name: 'CP Star' })).toBe('Auto-added by CP Star')
    expect(provenanceLine({ addition_method: 'company_auto_participant', triggering_company_name: null })).toBe('Auto-added by company policy')
    expect(provenanceLine({ addition_method: 'manual_invite' })).toBeNull()
    expect(provenanceLine({ addition_method: null })).toBeNull()
  })
})

describe('canRemoveParticipant (D17)', () => {
  const personalRow = { user_id: 'B', role: 'dispatcher' as const, addition_method: 'personal_auto_participant' as const, triggering_user_id: 'A', triggering_company_id: null }
  const companyRow = { user_id: 'S', role: 'dispatcher' as const, addition_method: 'company_auto_participant' as const, triggering_user_id: 'A', triggering_company_id: 'X' }
  const manualDriver = { user_id: 'D', role: 'driver' as const, addition_method: 'manual_invite' as const, triggering_user_id: null, triggering_company_id: null }

  it('a platform admin removes anyone', () => {
    expect(canRemoveParticipant({ viewer: { userId: 'Z', role: 'admin', isAdmin: true }, target: personalRow })).toBe(true)
    expect(canRemoveParticipant({ viewer: { userId: 'Z', role: 'shipper', isAdmin: true }, target: companyRow })).toBe(true)
  })
  it('the invite matrix still applies (a dispatcher removes a driver, not another dispatcher)', () => {
    expect(canRemoveParticipant({ viewer: { userId: 'Z', role: 'dispatcher', isAdmin: false }, target: manualDriver })).toBe(true)
    expect(canRemoveParticipant({ viewer: { userId: 'Z', role: 'dispatcher', isAdmin: false }, target: personalRow })).toBe(false)
    expect(canRemoveParticipant({ viewer: { userId: 'Z', role: 'driver', isAdmin: false }, target: personalRow })).toBe(true)
  })
  it('the person whose participation fired a personal rule may remove the row', () => {
    expect(canRemoveParticipant({ viewer: { userId: 'A', role: 'dispatcher', isAdmin: false }, target: personalRow })).toBe(true)
    expect(canRemoveParticipant({ viewer: { userId: 'A', role: 'dispatcher', isAdmin: false }, target: companyRow })).toBe(false)
  })
  it('a Company Admin acting in the triggering company removes a company-rule row', () => {
    expect(canRemoveParticipant({ viewer: { userId: 'M', role: 'dispatcher', isAdmin: false, activeCompanyId: 'X', activeIsCompanyAdmin: true }, target: companyRow })).toBe(true)
    expect(canRemoveParticipant({ viewer: { userId: 'M', role: 'dispatcher', isAdmin: false, activeCompanyId: 'Y', activeIsCompanyAdmin: true }, target: companyRow })).toBe(false)
    expect(canRemoveParticipant({ viewer: { userId: 'M', role: 'dispatcher', isAdmin: false, activeCompanyId: 'X', activeIsCompanyAdmin: false }, target: companyRow })).toBe(false)
    expect(canRemoveParticipant({ viewer: { userId: 'M', role: 'dispatcher', isAdmin: false, activeCompanyId: 'X', activeIsCompanyAdmin: true }, target: personalRow })).toBe(false)
  })
  it('nobody removes their own row through this path', () => {
    expect(canRemoveParticipant({ viewer: { userId: 'B', role: 'dispatcher', isAdmin: false }, target: personalRow })).toBe(false)
    expect(canRemoveParticipant({ viewer: { userId: 'D', role: 'driver', isAdmin: false }, target: { ...manualDriver, role: 'dispatcher' } })).toBe(false)
  })
})
