import { describe, expect, it } from 'vitest'
import {
  campaignReadiness, campaignTransition, dailyAllowance, describeSegment, filtersFromParams, filtersToParams, frequencyCapOk, funnelRates,
  leadMatchesFilters, leadSendBlock, leadStage, nextWindowStart, normalizeSteps, normalizeWindow, sendingDayIndex, splitName, stepDueAt, timezoneForLead, windowOpen,
} from '@/lib/domain/broker-leads'
import type { BrokerAgentLead } from '@/types/db'

/**
 * Freight Broker Leads (Nash, 2026-09-23): "manage the leads, send them
 * invites… create campaigns… automate emails and schedule emails and put
 * how many to go at the time."
 */

const base = {
  claim_status: 'unclaimed' as const, relationship_confirmation_status: 'not_confirmed' as const, invitation_status: 'not_invited' as const,
  signed_up_at: null, bounced_at: null, unsubscribed_at: null, suppressed_at: null, last_opened_at: null, last_clicked_at: null, last_emailed_at: null,
}

describe('leadStage', () => {
  it('walks the funnel', () => {
    expect(leadStage(base)).toBe('not_invited')
    expect(leadStage({ ...base, last_emailed_at: '2026-09-23T10:00:00Z' })).toBe('invited')
    expect(leadStage({ ...base, last_emailed_at: '2026-09-23T10:00:00Z', last_opened_at: '2026-09-23T11:00:00Z' })).toBe('opened')
    expect(leadStage({ ...base, last_opened_at: 'x', last_clicked_at: 'y' })).toBe('clicked')
    expect(leadStage({ ...base, last_clicked_at: 'y', signed_up_at: 'z' })).toBe('signed_up')
    expect(leadStage({ ...base, claim_status: 'claimed', relationship_confirmation_status: 'confirmed_current' })).toBe('converted')
    expect(leadStage({ ...base, claim_status: 'claimed', relationship_confirmation_status: 'no_longer_current' })).toBe('declined')
  })
  it('terminal states win over engagement', () => {
    expect(leadStage({ ...base, last_clicked_at: 'y', bounced_at: 'b' })).toBe('bounced')
    expect(leadStage({ ...base, last_clicked_at: 'y', unsubscribed_at: 'u' })).toBe('unsubscribed')
    expect(leadStage({ ...base, invitation_status: 'opted_out' })).toBe('unsubscribed')
    expect(leadStage({ ...base, suppressed_at: 's', last_opened_at: 'o' })).toBe('suppressed')
    expect(leadStage({ ...base, claim_status: 'manual_review' })).toBe('manual_review')
  })
  it('send block follows the stage', () => {
    expect(leadSendBlock(base)).toBeNull()
    expect(leadSendBlock({ ...base, last_emailed_at: 'x' })).toBeNull()
    expect(leadSendBlock({ ...base, claim_status: 'claimed', relationship_confirmation_status: 'confirmed_current' })).toBe('already_converted')
    expect(leadSendBlock({ ...base, signed_up_at: 'z' })).toBe('already_signed_up')
    expect(leadSendBlock({ ...base, bounced_at: 'b' })).toBe('bounced')
  })
})

describe('segments', () => {
  it('round-trips through the URL', () => {
    const f = filtersFromParams((k) => ({ q: 'tql', state: 'tx,fl', stage: 'not_invited,bogus', domain: 'matches_brokerage', mailbox: 'person', tag: 'wave-1', has_last_name: '1', seen_after: '2024-01-01' } as Record<string, string>)[k] ?? null)
    expect(f).toEqual({ q: 'tql', states: ['TX', 'FL'], stages: ['not_invited'], domain_check: ['matches_brokerage'], mailbox_type: ['person'], tags: ['wave-1'], has_last_name: true, last_seen_after: '2024-01-01' })
    const again = filtersFromParams((k) => new URLSearchParams(filtersToParams(f)).get(k))
    expect(again).toEqual(f)
  })
  it('describes itself in plain English', () => {
    expect(describeSegment({})).toBe('every lead')
    expect(describeSegment({ states: ['TX'], stages: ['not_invited'], has_last_name: true, last_seen_after: '2024-01-01' })).toBe('in TX, not invited, with a last name, active since 2024-01-01')
  })
  it('matches rows', () => {
    const lead = { ...base, id: '1', first_name: 'Ana', last_name: 'Ruiz', email: 'ana@tql.com', email_normalized: 'ana@tql.com', phone: null, phone_digits: null, broker_company_id: 'c1', broker_company_name: 'TQL', mc_number: '123', usdot_number: null, historical_relationship_verified: true, historical_verification_source: 'x', historical_verified_at: null, invited_at: null, claimed_user_id: null, claimed_at: null, confirmed_at: null, manual_review_reason: null, import_batch: 'b1', source_row: null, created_at: '', updated_at: '', state: 'OH', mailbox_type: 'person', email_domain_check: 'matches_brokerage', tags: ['wave-1'], source_last_seen_at: '2025-03-01T00:00:00Z' } as BrokerAgentLead & Record<string, unknown>
    expect(leadMatchesFilters(lead, { q: 'tql', states: ['OH'], stages: ['not_invited'], tags: ['wave-1'], has_last_name: true, last_seen_after: '2025-01-01' })).toBe(true)
    expect(leadMatchesFilters(lead, { states: ['TX'] })).toBe(false)
    expect(leadMatchesFilters(lead, { mailbox_type: ['role'] })).toBe(false)
    expect(leadMatchesFilters(lead, { last_seen_after: '2026-01-01' })).toBe(false)
    expect(leadMatchesFilters(lead, { q: 'nobody' })).toBe(false)
  })
})

describe('send window', () => {
  const w = normalizeWindow({ days: [1, 2, 3, 4, 5], start_hour: 9, end_hour: 17, timezone: 'lead' })
  it('resolves the zone from the lead state, or a fixed zone', () => {
    expect(timezoneForLead('TX', w)).toBe('America/Chicago')
    expect(timezoneForLead(null, w)).toBe('America/New_York')
    expect(timezoneForLead('TX', { ...w, timezone: 'America/Denver' })).toBe('America/Denver')
  })
  it('knows business hours in the lead zone', () => {
    // Wed 2026-09-23 15:00 UTC = 10:00 Chicago (open), 08:00 Los Angeles (closed)
    const at = new Date('2026-09-23T15:00:00Z')
    expect(windowOpen(w, at, 'America/Chicago')).toBe(true)
    expect(windowOpen(w, at, 'America/Los_Angeles')).toBe(false)
    // Saturday
    expect(windowOpen(w, new Date('2026-09-26T15:00:00Z'), 'America/Chicago')).toBe(false)
  })
  it('moves a due time into the next open window', () => {
    // Friday 2026-09-25 23:30 UTC (18:30 Chicago, closed) → Monday 09:00 Chicago = 14:00 UTC
    const next = nextWindowStart(w, new Date('2026-09-25T23:30:00Z'), 'America/Chicago')
    expect(next.toISOString()).toBe('2026-09-28T14:00:00.000Z')
    // already open → unchanged
    const open = new Date('2026-09-23T15:00:00Z')
    expect(nextWindowStart(w, open, 'America/Chicago').toISOString()).toBe(open.toISOString())
    // before opening the same day → that day's 09:00
    expect(nextWindowStart(w, new Date('2026-09-23T11:00:00Z'), 'America/Chicago').toISOString()).toBe('2026-09-23T14:00:00.000Z')
  })
  it('a follow-up step waits its delay, then the window', () => {
    // sent Wed 2026-09-23 15:00 UTC, 7 days → Wed 2026-09-30 15:00 UTC = 10:00 Chicago, open
    expect(stepDueAt(new Date('2026-09-23T15:00:00Z'), 7, w, 'America/Chicago').toISOString()).toBe('2026-09-30T15:00:00.000Z')
    // 3 days from Thursday → Sunday → Monday 09:00 Chicago
    expect(stepDueAt(new Date('2026-09-24T15:00:00Z'), 3, w, 'America/Chicago').toISOString()).toBe('2026-09-28T14:00:00.000Z')
  })
  it('normalizes nonsense', () => {
    expect(normalizeWindow(null)).toEqual({ days: [1, 2, 3, 4, 5], start_hour: 9, end_hour: 17, timezone: 'lead' })
    expect(normalizeWindow({ days: [9, 0, 0], start_hour: 20, end_hour: 8 })).toEqual({ days: [0], start_hour: 20, end_hour: 21, timezone: 'lead' })
  })
})

describe('throttle', () => {
  it('warm-up ramp grows daily and never exceeds the limit', () => {
    const c = { daily_send_limit: 200, ramp: { start: 50, step: 50, max: 300 } }
    expect(dailyAllowance(c, 0)).toBe(50)
    expect(dailyAllowance(c, 1)).toBe(100)
    expect(dailyAllowance(c, 3)).toBe(200)
    expect(dailyAllowance(c, 10)).toBe(200)
    expect(dailyAllowance({ daily_send_limit: 80, ramp: null }, 5)).toBe(80)
  })
  it('counts sending days since the first run', () => {
    expect(sendingDayIndex(null, '2026-09-23')).toBe(0)
    expect(sendingDayIndex('2026-09-23', '2026-09-23')).toBe(0)
    expect(sendingDayIndex('2026-09-20', '2026-09-23')).toBe(3)
  })
  it('frequency cap: one email per lead per N days across campaigns', () => {
    const now = new Date('2026-09-23T15:00:00Z')
    expect(frequencyCapOk(null, 7, now)).toBe(true)
    expect(frequencyCapOk('2026-09-20T15:00:00Z', 7, now)).toBe(false)
    expect(frequencyCapOk('2026-09-16T15:00:00Z', 7, now)).toBe(true)
    expect(frequencyCapOk('2026-09-22T15:00:00Z', 0, now)).toBe(true)
  })
})

describe('campaign lifecycle', () => {
  it('transitions', () => {
    expect(campaignTransition('draft', 'approve')).toEqual({ ok: true, to: 'running' })
    expect(campaignTransition('running', 'pause')).toEqual({ ok: true, to: 'paused' })
    expect(campaignTransition('paused', 'resume')).toEqual({ ok: true, to: 'running' })
    expect(campaignTransition('running', 'stop')).toEqual({ ok: true, to: 'stopped' })
    expect(campaignTransition('completed', 'stop').ok).toBe(false)
    expect(campaignTransition('draft', 'pause').ok).toBe(false)
  })
  it('readiness lists every problem', () => {
    const problems = campaignReadiness({ name: '', steps: [], start_date: null, daily_send_limit: 0, enrolled: 0, templatesActive: {} })
    expect(problems).toHaveLength(5)
    expect(campaignReadiness({ name: 'Wave 1', steps: normalizeSteps([{ template_key: 'broker_lead_invitation', delay_days: 0 }]), start_date: '2026-09-24', daily_send_limit: 50, enrolled: 10, templatesActive: { broker_lead_invitation: true } })).toEqual([])
    expect(campaignReadiness({ name: 'Wave 1', steps: normalizeSteps([{ template_key: 'gone', delay_days: 0 }]), start_date: '2026-09-24', daily_send_limit: 50, enrolled: 10, templatesActive: { broker_lead_invitation: false } })).toEqual(['Template "gone" does not exist.'])
  })
  it('steps are renumbered and blanks dropped', () => {
    expect(normalizeSteps([{ template_key: 'a', delay_days: -3 }, { template_key: '' }, { template_key: 'b', delay_days: 7 }])).toEqual([{ step: 1, template_key: 'a', delay_days: 0 }, { step: 2, template_key: 'b', delay_days: 7 }])
  })
})

describe('funnel + names', () => {
  it('rates', () => {
    const r = funnelRates({ enrolled: 100, sent: 80, delivered: 76, opened: 38, clicked: 19, signed_up: 8, converted: 6, bounced: 4, unsubscribed: 1 })
    expect(r.delivered_pct).toBe(95)
    expect(r.open_pct).toBe(50)
    expect(r.conversion_pct).toBe(7.5)
    expect(funnelRates({ enrolled: 0, sent: 0, delivered: 0, opened: 0, clicked: 0, signed_up: 0, converted: 0, bounced: 0, unsubscribed: 0 }).open_pct).toBe(0)
  })
  it('splits names', () => {
    expect(splitName('Terry')).toEqual({ first: 'Terry', last: '' })
    expect(splitName('Mary Ann Smith')).toEqual({ first: 'Mary', last: 'Ann Smith' })
    expect(splitName('  Lopez,  Maria ')).toEqual({ first: 'Lopez', last: 'Maria' })
  })
})

/* ---------------------------------------------------------- two kinds */

import { CARRIER_LEAD_TEMPLATE_VARIABLES, LEAD_KINDS, carrierLeadRole, leadKindConfig, leadTemplateData, leadTemplateVariables } from '@/lib/domain/broker-leads'

describe('carrier leads are the same engine with kind = carrier (2026-09-25)', () => {
  const carrier = { ...base, lead_kind: 'carrier' as const }
  it('signing up IS the conversion for a carrier lead; a broker lead still needs the relation confirmed', () => {
    expect(leadStage({ ...carrier, signed_up_at: '2026-09-25T00:00:00Z' })).toBe('converted')
    expect(leadStage({ ...base, lead_kind: 'broker', signed_up_at: '2026-09-25T00:00:00Z' })).toBe('signed_up')
    expect(leadSendBlock({ ...carrier, signed_up_at: 'x' })).toBe('already_converted')
  })
  it('filters by what the person did and whether they are still active in the source', () => {
    const l = (over: Record<string, unknown>) => ({ ...(base as unknown as BrokerAgentLead), id: '1', email: 'x@y.com', email_normalized: 'x@y.com', lead_kind: 'carrier', ...over } as BrokerAgentLead & { lead_type?: string | null; source_status_active?: boolean | null; company_name?: string | null })
    expect(leadMatchesFilters(l({ lead_type: 'driver' }), { lead_type: ['driver'] })).toBe(true)
    expect(leadMatchesFilters(l({ lead_type: 'dispatcher' }), { lead_type: ['driver'] })).toBe(false)
    expect(leadMatchesFilters(l({ lead_type: 'both', source_status_active: false }), { source_active: true })).toBe(false)
    expect(leadMatchesFilters(l({ company_name: 'ABC TRANSPORT LLC' }), { brokerage_id: 'ABC TRANSPORT LLC' })).toBe(true)
    expect(leadMatchesFilters(l({ company_name: 'ABC TRANSPORT LLC' }), { q: 'abc transport' })).toBe(true)
  })
  it('the URL round-trips the carrier filters', () => {
    const f = filtersFromParams((k) => ({ type: 'driver,both', active: '1', stage: 'not_invited' } as Record<string, string>)[k] ?? null)
    expect(f).toEqual({ lead_type: ['driver', 'both'], source_active: true, stages: ['not_invited'] })
    expect(filtersToParams(f)).toBe('stage=not_invited&type=driver%2Cboth&active=1')
    expect(describeSegment(f)).toBe('not invited, driver or dispatcher + driver, still active in the source')
  })
  it('each kind has its own page, API, surface, campaign type, category, settings row and templates', () => {
    expect(LEAD_KINDS.carrier.path).toBe('/admin/carrier-leads')
    expect(LEAD_KINDS.carrier.apiBase).toBe('/api/admin/carrier-leads')
    expect(LEAD_KINDS.carrier.campaignType).toBe('carrier_recruitment')
    expect(LEAD_KINDS.carrier.category).toBe('carrier_opportunities')
    expect(LEAD_KINDS.carrier.settingsId).toBe(2)
    expect(LEAD_KINDS.broker.settingsId).toBe(1)
    expect(leadKindConfig(undefined).kind).toBe('broker')
    expect(leadTemplateVariables('carrier')).toBe(CARRIER_LEAD_TEMPLATE_VARIABLES)
    expect(leadTemplateVariables('carrier')).toContain('company_name')
    expect(leadTemplateVariables('carrier')).not.toContain('mc_number')
  })
  it('template data carries the company and what the person did', () => {
    const d = leadTemplateData({ first_name: 'Sam', last_name: 'Rivera', broker_company_name: null, mc_number: null, email: 's@x.com', company_name: 'ABC TRANSPORT LLC', lead_type: 'both' }, 'https://x/join/t')
    expect(d.company_name).toBe('ABC TRANSPORT LLC')
    expect(d.lead_type).toBe('Dispatcher + driver')
    expect(d.brokerage_name).toBe('')
  })
  it('a carrier lead becomes a dispatcher or driver account and lands on that dashboard', () => {
    expect(carrierLeadRole('driver')).toEqual({ role: 'driver', next: '/ct-dashboard' })
    expect(carrierLeadRole('dispatcher')).toEqual({ role: 'dispatcher', next: '/cd-dashboard' })
    expect(carrierLeadRole('both')).toEqual({ role: 'dispatcher', next: '/cd-dashboard' })
    expect(carrierLeadRole(null)).toEqual({ role: 'dispatcher', next: '/cd-dashboard' })
  })
})

/* ------------------------------------------- carrier campaign copy */

import { DEFAULT_TEMPLATES } from '@/lib/email-templates'
import { CARRIER_LEAD_TEMPLATE_KEYS } from '@/lib/domain/broker-leads'
import { TEMPLATE_CATEGORY } from '@/lib/domain/email-policy'

/**
 * Nash, 2026-09-25: "they did create an account on Synchron, but they never used
 * us… we should not advertise as a Synchron." The carrier campaign sells what the
 * free account does — upload a permit, alerts, provisions in plain language, the
 * AI chat, Google Maps routes — and never names the permit service or claims a
 * past relationship.
 */
describe('carrier lead templates', () => {
  const carrierTemplates = CARRIER_LEAD_TEMPLATE_KEYS.map((k) => {
    const t = DEFAULT_TEMPLATES.find((x) => x.key === k)
    if (!t) throw new Error(`shipped template ${k} is missing`)
    return t
  })
  it('never mention the permit service and never claim previous business with us', () => {
    for (const t of carrierTemplates) {
      const text = `${t.subject} ${t.body} ${t.recipients_note ?? ''}`
      expect(text).not.toMatch(/synchron/i)
      expect(t.body).not.toMatch(/you have (ordered|used|worked)|we have worked with you|already set up|pre-verified/i)
    }
  })
  it('carry the join link and the free-account promise', () => {
    for (const t of carrierTemplates) expect(t.body).toContain('{{join_link}}')
    const invitation = carrierTemplates[0].body
    for (const point of ['free', 'alert', 'provision', 'Google Maps', 'GPX']) expect(invitation).toContain(point)
    expect(invitation).toMatch(/only pay when you order a route/i)
  })
  it('use no variable that can render empty, so no line is ever dropped', () => {
    for (const t of carrierTemplates) {
      const used = [...`${t.subject}\n${t.body}`.matchAll(/\{\{(\w+)\}\}/g)].map((m) => m[1])
      // first_name falls back to "there"; the link, support address and footer are always present.
      expect([...new Set(used)].sort()).toEqual(['company_footer', 'first_name', 'join_link', 'support_email'])
    }
  })
  it('are updates-stream carrier invitations, so unsubscribe always applies', () => {
    for (const k of CARRIER_LEAD_TEMPLATE_KEYS) expect(TEMPLATE_CATEGORY[k]).toBe('carrier_opportunities')
  })
})
