// qaf-source.ts tests (QVS-P2, KAR-971): DB-row loading + the
// previewToken/mapAndFingerprint fingerprinting that both preview.ts and
// creation.ts rely on to agree on "has the source changed".

import { describe, it, expect, vi } from 'vitest'
import { computePreviewToken, loadQafSourceRows, mapAndFingerprint, type QafSourceRows } from '../qaf-source'
import type { QAFRow } from '@/lib/qaf-parser'

function makeRow(overrides: Partial<QAFRow> = {}): QAFRow {
  return {
    positionsnummer: '',
    teilebenennung: '',
    prozessbezeichnung: '',
    bezeichnungAnlage: '',
    standort: '',
    beschaffungswaehrung: '',
    zykluszeit: null,
    teileProZyklus: null,
    anzahlMA: null,
    lohnkosten: null,
    lohnzuschlagssaetze: null,
    mss: null,
    ruestkosten: null,
    fek: null,
    rfgk: null,
    fk: null,
    angebotswaehrung: '',
    wechselkurs: null,
    anzahlProAngebotsteil: null,
    fkAW: null,
    ausschuss: null,
    ausschusskosten: null,
    ...overrides,
  }
}

describe('computePreviewToken', () => {
  const base = { qafFileId: 'file-1', fileHash: 'hash-1', parserVersion: 'p1', mappingVersion: 'qvs-1', includedRowIndexes: [0, 1, 2] }

  it('is deterministic for identical input', () => {
    expect(computePreviewToken(base)).toBe(computePreviewToken({ ...base }))
  })

  it('is independent of includedRowIndexes order', () => {
    expect(computePreviewToken({ ...base, includedRowIndexes: [2, 0, 1] })).toBe(computePreviewToken(base))
  })

  it('changes when the included row set changes (a step disappeared from the source)', () => {
    expect(computePreviewToken({ ...base, includedRowIndexes: [0, 1] })).not.toBe(computePreviewToken(base))
  })

  it('changes when fileHash changes (file replaced)', () => {
    expect(computePreviewToken({ ...base, fileHash: 'hash-2' })).not.toBe(computePreviewToken(base))
  })

  it('changes when mappingVersion changes', () => {
    expect(computePreviewToken({ ...base, mappingVersion: 'qvs-2' })).not.toBe(computePreviewToken(base))
  })

  it('treats null and empty-string fileHash/parserVersion the same (both fold to "")', () => {
    expect(computePreviewToken({ ...base, fileHash: null })).toBe(computePreviewToken({ ...base, fileHash: '' }))
  })
})

describe('mapAndFingerprint', () => {
  const source: QafSourceRows = {
    qafFile: {
      id: 'file-1',
      projectId: 'project-1',
      fileHash: 'hash-1',
      originalFileName: 'x.xlsx',
      parserVersion: 'p1',
      fileLevelContext: { plannedCapacityPartsPerYear: null, lotSizeParts: null },
    },
    rows: [makeRow({ prozessbezeichnung: 'Schweißen' }), makeRow({ prozessbezeichnung: '' }), makeRow({ prozessbezeichnung: 'Montage' })],
    stepIds: ['step-0', 'step-1', 'step-2'],
  }

  it('excludes the row with no prozessbezeichnung and backfills manufacturingStepId by rowIndex', () => {
    const { mapped } = mapAndFingerprint(source)
    expect(mapped.nodes).toHaveLength(2)
    expect(mapped.excludedRows).toEqual([{ rowIndex: 1, reason: 'missing_process_name' }])
    expect(mapped.nodes[0].qafSource?.rowIndex).toBe(0)
    expect(mapped.nodes[0].qafSource?.manufacturingStepId).toBe('step-0')
    expect(mapped.nodes[1].qafSource?.rowIndex).toBe(2)
    expect(mapped.nodes[1].qafSource?.manufacturingStepId).toBe('step-2')
  })

  it('produces the same previewToken for the same source regardless of importId', () => {
    const a = mapAndFingerprint(source, { importId: 'import-a' })
    const b = mapAndFingerprint(source, { importId: 'import-b' })
    expect(a.previewToken).toBe(b.previewToken)
  })

  it('changes previewToken when a row that used to qualify no longer does', () => {
    const shrunk: QafSourceRows = { ...source, rows: [source.rows[0], source.rows[1], makeRow({ prozessbezeichnung: '' })] }
    const a = mapAndFingerprint(source)
    const b = mapAndFingerprint(shrunk)
    expect(a.previewToken).not.toBe(b.previewToken)
  })
})

describe('loadQafSourceRows', () => {
  function makeSupabase(opts: {
    fileRow?: Record<string, unknown> | null
    fileError?: unknown
    stepRows?: Array<{ id: string; raw_values: unknown }>
    stepsError?: unknown
    summaryMetricRows?: Array<{ metric_key: string; value: number | string | null }>
    summaryMetricError?: unknown
  }) {
    return {
      from: vi.fn((table: string) => {
        if (table === 'qaf_file') {
          return {
            select: vi.fn(() => ({
              eq: vi.fn(() => ({
                maybeSingle: vi.fn(() => Promise.resolve({ data: opts.fileRow ?? null, error: opts.fileError ?? null })),
              })),
            })),
          }
        }
        if (table === 'qaf_manufacturing_step') {
          return {
            select: vi.fn(() => ({
              eq: vi.fn(() => ({
                order: vi.fn(() => ({
                  order: vi.fn(() => Promise.resolve({ data: opts.stepRows ?? [], error: opts.stepsError ?? null })),
                })),
              })),
            })),
          }
        }
        // QVS-P4: loadQvsFileLevelContext's qaf_summary_metric read — every
        // loadQafSourceRows call now issues this alongside qaf_manufacturing_step
        // (Promise.all), so every test in this describe block needs it handled,
        // not just the ones that care about its result.
        if (table === 'qaf_summary_metric') {
          return {
            select: vi.fn(() => ({
              eq: vi.fn(() => ({
                in: vi.fn(() => Promise.resolve({ data: opts.summaryMetricRows ?? [], error: opts.summaryMetricError ?? null })),
              })),
            })),
          }
        }
        throw new Error(`unexpected table queried: ${table}`)
      }),
    }
  }

  it('returns null when the qaf_file is not found (or not visible via RLS) — same result either way', async () => {
    const supabase = makeSupabase({ fileRow: null })
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const result = await loadQafSourceRows(supabase as any, 'file-1')
    expect(result).toBeNull()
  })

  it('returns null on a qaf_manufacturing_step query error (fails closed, no partial result)', async () => {
    const supabase = makeSupabase({
      fileRow: { id: 'file-1', project_id: 'project-1', file_hash: 'hash-1', original_file_name: 'x.xlsx', parser_version: 'p1' },
      stepsError: { message: 'boom' },
    })
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const result = await loadQafSourceRows(supabase as any, 'file-1')
    expect(result).toBeNull()
  })

  it('maps qaf_file + ordered qaf_manufacturing_step rows into QafSourceRows', async () => {
    const row0 = makeRow({ prozessbezeichnung: 'Schweißen' })
    const row1 = makeRow({ prozessbezeichnung: 'Montage' })
    const supabase = makeSupabase({
      fileRow: { id: 'file-1', project_id: 'project-1', file_hash: 'hash-1', original_file_name: 'x.xlsx', parser_version: 'p1' },
      stepRows: [
        { id: 'step-0', raw_values: row0 },
        { id: 'step-1', raw_values: row1 },
      ],
    })
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const result = await loadQafSourceRows(supabase as any, 'file-1')
    expect(result).not.toBeNull()
    expect(result?.qafFile).toEqual({
      id: 'file-1',
      projectId: 'project-1',
      fileHash: 'hash-1',
      originalFileName: 'x.xlsx',
      parserVersion: 'p1',
      fileLevelContext: { plannedCapacityPartsPerYear: null, lotSizeParts: null },
    })
    expect(result?.rows).toEqual([row0, row1])
    expect(result?.stepIds).toEqual(['step-0', 'step-1'])
  })

  describe('fileLevelContext (QVS-P4, sum_planned_capacity/sum_lot_size)', () => {
    const FILE_ROW = { id: 'file-1', project_id: 'project-1', file_hash: 'hash-1', original_file_name: 'x.xlsx', parser_version: 'p1' }

    it('reads both counts from qaf_summary_metric rows', async () => {
      const supabase = makeSupabase({
        fileRow: FILE_ROW,
        stepRows: [],
        summaryMetricRows: [
          { metric_key: 'plannedCapacity', value: 221500 },
          { metric_key: 'lotSize', value: 250 },
        ],
      })
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      const result = await loadQafSourceRows(supabase as any, 'file-1')
      expect(result?.qafFile.fileLevelContext).toEqual({ plannedCapacityPartsPerYear: 221500, lotSizeParts: 250 })
    })

    it('coerces a Postgres NUMERIC returned as a string (precision round-trip)', async () => {
      const supabase = makeSupabase({
        fileRow: FILE_ROW,
        stepRows: [],
        summaryMetricRows: [
          { metric_key: 'plannedCapacity', value: '221500' },
          { metric_key: 'lotSize', value: '250' },
        ],
      })
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      const result = await loadQafSourceRows(supabase as any, 'file-1')
      expect(result?.qafFile.fileLevelContext).toEqual({ plannedCapacityPartsPerYear: 221500, lotSizeParts: 250 })
    })

    it('defaults to null (never 0) when no qaf_summary_metric rows exist for these keys', async () => {
      const supabase = makeSupabase({ fileRow: FILE_ROW, stepRows: [], summaryMetricRows: [] })
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      const result = await loadQafSourceRows(supabase as any, 'file-1')
      expect(result?.qafFile.fileLevelContext).toEqual({ plannedCapacityPartsPerYear: null, lotSizeParts: null })
    })

    it('degrades to null (never fails the whole load) on a qaf_summary_metric query error', async () => {
      const supabase = makeSupabase({ fileRow: FILE_ROW, stepRows: [], summaryMetricError: { message: 'boom', code: '500' } })
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      const result = await loadQafSourceRows(supabase as any, 'file-1')
      expect(result).not.toBeNull()
      expect(result?.qafFile.fileLevelContext).toEqual({ plannedCapacityPartsPerYear: null, lotSizeParts: null })
    })

    it('only one of the two counts present still reports the other as null', async () => {
      const supabase = makeSupabase({
        fileRow: FILE_ROW,
        stepRows: [],
        summaryMetricRows: [{ metric_key: 'plannedCapacity', value: 100 }],
      })
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      const result = await loadQafSourceRows(supabase as any, 'file-1')
      expect(result?.qafFile.fileLevelContext).toEqual({ plannedCapacityPartsPerYear: 100, lotSizeParts: null })
    })
  })

  it('defaults a missing file_hash/parser_version to null rather than undefined', async () => {
    const supabase = makeSupabase({
      fileRow: { id: 'file-1', project_id: 'project-1', file_hash: null, original_file_name: 'x.xlsx', parser_version: null },
      stepRows: [],
    })
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const result = await loadQafSourceRows(supabase as any, 'file-1')
    expect(result?.qafFile.fileHash).toBeNull()
    expect(result?.qafFile.parserVersion).toBeNull()
  })
})
