import { createHash } from 'node:crypto'
import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest'

const mocks = vi.hoisted(() => ({ admin: vi.fn(), extract: vi.fn(), sync: vi.fn(), alert: vi.fn() }))
vi.mock('@/lib/supabase/admin', () => ({ createAdminClient: mocks.admin }))
vi.mock('@/lib/adapters/flask', () => ({ extractPermit: mocks.extract }))
vi.mock('@/lib/integrations/hha-trip-sync', () => ({ syncTripToHha: mocks.sync }))
vi.mock('@/lib/data/permit-dimension-alert', () => ({ sendPermitDimensionAlerts: mocks.alert }))

import { permitDownloadAllowed, permitExtractionFields, processWorkspacePermit } from '@/lib/data/permit-processing'
import { permitRouteLabel, permitRouteRequest } from '@/lib/domain/permit-routes'
import type { Permit } from '@/types/db'

const file = new Blob(['%PDF-1.7 test permit'])
let permit: Permit
let notify: boolean
let claim: boolean
let publish: boolean
let writes: Array<{ table: string; data: Record<string, unknown> }>
let published: Record<string, unknown>[]
let rpc: ReturnType<typeof vi.fn>

function builder(table: string) {
  // These are the ONLY collections this worker may access. In particular no
  // service_requests, credits, balances or billing writes are accepted.
  expect(['permits', 'documents', 'trips', 'permit_processing_jobs']).toContain(table)
  let single = false
  const q = {
    select: () => q, eq: () => q,
    single: () => { single = true; return q },
    upsert: (data: Record<string, unknown>) => { writes.push({ table, data }); return q },
    update: (data: Record<string, unknown>) => { writes.push({ table, data }); return q },
    then: (resolve: (value: unknown) => void) => {
      const data = table === 'permits' ? (single ? permit : [permit])
        : table === 'documents' ? { id: 'doc', trip_id: 'trip', file_name: 'permit.pdf', external_url: 'https://heavy-haul-agent.s3.us-east-1.amazonaws.com/permits/orderSteptwo/test.pdf' }
          : table === 'trips' ? { id: 'trip', status: 'active', load_width_in: 130, load_height_in: null, load_length_in: null, load_weight_lbs: null }
            : null
      resolve({ data, error: null })
    },
  }
  return q
}

beforeEach(() => {
  vi.resetAllMocks()
  vi.stubEnv('WORKSPACE_DIMENSION_ALERTS_ENABLED', 'true')
  vi.spyOn(console, 'error').mockImplementation(() => {})
  permit = { id: 'permit', trip_id: 'trip', document_id: 'doc', state_code: 'TX', extraction_status: 'pending' } as Permit
  notify = true; claim = true; publish = true; writes = []; published = []
  rpc = vi.fn(async (name: string, args: Record<string, unknown>) => {
    if (name === 'claim_workspace_permit') return { error: null, data: claim ? [{ permit_id: 'permit', lease_token: 'lease', revision: 1, attempts: 1, notify_contacts: notify }] : [] }
    expect(name).toBe('publish_workspace_permit')
    published.push(args)
    return { error: null, data: publish }
  })
  mocks.admin.mockReturnValue({ from: builder, rpc })
  mocks.sync.mockResolvedValue({ ok: true })
  mocks.extract.mockResolvedValue({ ok: true, data: { state_code: 'TX', width_in: 120, route_token: '123', route_status: 'needs_preparation' } })
  mocks.alert.mockResolvedValue({ sent: 1, skipped: 0, failed: 0 })
})
afterEach(() => { vi.restoreAllMocks(); vi.unstubAllEnvs() })

describe('common Workspace permit processor', () => {
  it.each([null, 'synchron'])('extracts, checks dimensions and syncs without billing (source %s)', async (source) => {
    permit.source_system = source
    const result = await processWorkspacePermit({ permitId: 'permit', origin: 'https://workspace.test', file })
    expect(result.ok).toBe(true)
    expect(mocks.extract.mock.calls[0][2]).toMatchObject({ tripId: 'trip', documentId: 'doc', permitId: 'permit' })
    expect(published[0].p_result).toMatchObject({ route_token: '123', route_processing_status: 'needs_preparation', permit_width_in: 120 })
    expect(published[0].p_warnings).toEqual(expect.arrayContaining([expect.objectContaining({ kind: 'dimension_mismatch' })]))
    expect(mocks.sync).toHaveBeenCalledTimes(2)
    expect(mocks.alert).toHaveBeenCalledOnce()
    expect(writes.at(-1)).toMatchObject({ table: 'permit_processing_jobs', data: { status: 'done' } })
  })

  it('keeps a failed extraction retryable, without losing the permit or mailing', async () => {
    mocks.extract.mockResolvedValue({ ok: false, error: 'provider detail not logged' })
    const result = await processWorkspacePermit({ permitId: 'permit', origin: 'https://workspace.test', file })
    expect(result).toMatchObject({ ok: false, error: 'permit_extraction_unavailable' })
    expect(published).toHaveLength(0)
    expect(mocks.alert).not.toHaveBeenCalled()
    expect(writes.at(-1)?.data).toMatchObject({ status: 'retry', last_error: 'permit_extraction_unavailable' })
  })

  it('waits for GPT sync before extraction', async () => {
    mocks.sync.mockResolvedValue({ ok: false })
    expect((await processWorkspacePermit({ origin: 'https://workspace.test', file })).ok).toBe(false)
    expect(mocks.extract).not.toHaveBeenCalled()
  })

  it('reuses a cached token when retrying sync/email without calling GPT again', async () => {
    permit.extraction_status = 'processed'
    permit.route_processing_status = 'needs_preparation'
    permit.extraction = { state_code: 'TX', route_token: '123', route_status: 'needs_preparation', _workspace_file_sha256: createHash('sha256').update(Buffer.from(await file.arrayBuffer())).digest('hex') }
    expect((await processWorkspacePermit({ origin: 'https://workspace.test', file })).status).toBe('needs_preparation')
    expect(mocks.extract).not.toHaveBeenCalled()
    expect(writes.at(-1)?.data.status).toBe('done')
  })

  it('does not keep calling extraction just because no map links were returned', async () => {
    mocks.extract.mockResolvedValue({ ok: true, data: { state_code: 'TX', route_token: '123', route_status: 'needs_preparation' } })
    expect((await processWorkspacePermit({ origin: 'https://workspace.test', file })).status).toBe('needs_preparation')
    expect(writes.at(-1)?.data.status).toBe('done')
  })

  it('publishes a matched route link as ready without billing', async () => {
    mocks.extract.mockResolvedValue({ ok: true, data: {
      state_code: 'SC', route_token: '470154', route_source: 'synchron', route_status: 'ready',
      route_links: ['https://maps.example.test/part-1'],
    } })
    expect((await processWorkspacePermit({ origin: 'https://workspace.test', file })).status).toBe('ready')
    expect(published[0].p_result).toMatchObject({
      route_token: '470154', route_processing_status: 'ready',
      route_links: ['https://maps.example.test/part-1'],
    })
    expect(writes.every((write) => write.table === 'permit_processing_jobs')).toBe(true)
  })

  it('checks historical dimensions but does not send historical emails', async () => {
    notify = false
    expect((await processWorkspacePermit({ origin: 'https://workspace.test', file })).ok).toBe(true)
    expect(published[0].p_warnings).not.toEqual([])
    expect(mocks.alert).not.toHaveBeenCalled()
  })

  it('pauses new dimension warnings and emails without stopping extraction or other warnings', async () => {
    vi.stubEnv('WORKSPACE_DIMENSION_ALERTS_ENABLED', 'false')
    permit.expiration_date = '2020-01-01'
    const result = await processWorkspacePermit({ permitId: 'permit', origin: 'https://workspace.test', file })
    expect(result.ok).toBe(true)
    const warnings = published[0].p_warnings as Array<{ kind: string }>
    expect(warnings.some((warning) => warning.kind === 'dimension_mismatch')).toBe(false)
    expect(warnings.some((warning) => warning.kind === 'expired')).toBe(true)
    expect(mocks.alert).not.toHaveBeenCalled()
    expect(mocks.sync).toHaveBeenCalledTimes(2)
  })

  it('finishes an unsupported state without a token, still checking dimensions and notifying', async () => {
    mocks.extract.mockResolvedValue({ ok: true, data: { state_code: 'TX', width_in: 120, route_status: 'unsupported' } })
    expect((await processWorkspacePermit({ origin: 'https://workspace.test', file })).status).toBe('unsupported')
    expect(published[0].p_result).toMatchObject({ route_token: null, route_processing_status: 'unsupported' })
    expect(published[0].p_warnings).not.toEqual([])
    expect(mocks.alert).toHaveBeenCalledOnce()
    expect(writes.at(-1)?.data.status).toBe('done')
  })

  it('retries a route failure after saving extraction and running dimension checks', async () => {
    mocks.extract.mockResolvedValue({ ok: true, data: { state_code: 'TX', width_in: 120, route_status: 'failed' } })
    expect((await processWorkspacePermit({ origin: 'https://workspace.test', file })).error).toBe('route_processing_unavailable')
    expect(published).toHaveLength(1)
    expect(mocks.alert).toHaveBeenCalledOnce()
    expect(writes.at(-1)?.data.status).toBe('retry')
  })

  it('does nothing when another worker owns the job', async () => {
    claim = false
    expect((await processWorkspacePermit({ origin: 'https://workspace.test', file })).status).toBe('queued_or_already_processed')
    expect(mocks.extract).not.toHaveBeenCalled()
    expect(published).toHaveLength(0)
  })

  it('does not publish or email an extraction superseded by a new file', async () => {
    publish = false
    expect((await processWorkspacePermit({ origin: 'https://workspace.test', file })).status).toBe('superseded')
    expect(mocks.alert).not.toHaveBeenCalled()
    expect(mocks.sync).toHaveBeenCalledTimes(1)
  })

  it('rejects a Synchron state mismatch without changing the permit', async () => {
    permit.source_system = 'synchron'
    mocks.extract.mockResolvedValue({ ok: true, data: { state_code: 'UT', route_status: 'ready' } })
    expect((await processWorkspacePermit({ origin: 'https://workspace.test', file })).error).toBe('permit_state_mismatch')
    expect(published).toHaveLength(0)
  })

  it.each([new Blob([new Uint8Array([0x89, 0x50, 0x4e, 0x47, 1])]), new Blob([new Uint8Array([0xff, 0xd8, 0xff, 1])])])('accepts image bytes as well as PDF', async (image) => {
    expect((await processWorkspacePermit({ origin: 'https://workspace.test', file: image })).ok).toBe(true)
    expect(mocks.extract).toHaveBeenCalledOnce()
  })

  it('refuses non-permit content', async () => {
    expect((await processWorkspacePermit({ origin: 'https://workspace.test', file: new Blob(['<html>']) })).error).toBe('unsupported_permit_file')
    expect(mocks.extract).not.toHaveBeenCalled()
  })

  it('preserves manually corrected permit fees', () => {
    permit.permit_cost_status = 'corrected'
    expect(permitExtractionFields(permit, { permit_fee: 500 })).not.toHaveProperty('permit_cost_amount')
  })
})

describe('safe download and route presentation', () => {
  it.each(['http://localhost/a.pdf', 'https://attacker.test/a.pdf', 'https://heavy-haul-agent.s3.us-east-1.amazonaws.com@attacker.test/a.pdf'])('blocks unsafe URL %s', (url) => {
    expect(permitDownloadAllowed(url)).toBe(false)
  })
  it('allows only the configured file providers', () => {
    expect(permitDownloadAllowed('https://heavy-haul-agent.s3.us-east-1.amazonaws.com/permits/test.pdf')).toBe(true)
  })
  it('uses each permit’s own maps without a credit purchase', () => {
    const first = permitRouteRequest({ ...permit, route_processing_status: 'ready', route_links: ['https://maps.test/1'] })!
    const second = permitRouteRequest({ ...permit, id: 'second', route_processing_status: 'ready', route_links: ['https://maps.test/2'] })!
    expect(first.id).not.toBe(second.id)
    expect(first.route_links).not.toEqual(second.route_links)
    expect(first.paid_with).toBeNull()
    expect(permitRouteRequest({ ...permit, route_processing_status: 'needs_preparation' })).toBeUndefined()
    expect(permitRouteLabel('needs_preparation')).toContain('map preparation pending')
  })
})
