// reparseComparisonFile (Loop 5 Teil 2, SRC-001): die NEUE Logik des
// Wrappers — Modus-Gate (deferred-Grenze), Legacy-Quelle, Confused-Deputy-
// Prefix-Guard, Kopie-Semantik. Wichtigste Aussage dieser Datei: die
// ORIGINALDATEI wird in keinem Ausgang von storage.remove getroffen — nur
// die eigene Bucket-Kopie. Der Happy-Path-Durchstich (Parse+Persist) läuft
// über das produktiv bemusterte replaceComparisonFile und wird hier nicht
// dupliziert (Mock-Strategie wie get-calculator-params.test.ts).

import { describe, it, expect, vi, beforeEach } from 'vitest'

const VALID_UUID = '22222222-2222-4222-8222-222222222222'
const PROJECT_ID = '33333333-3333-4333-8333-333333333333'
const SOURCE_KEY = `${PROJECT_ID}/44444444-4444-4444-8444-444444444444-original.xlsx`

interface MockConfig {
  comparisonMode: string | null
  storageKey: string | null
  baselineFileId: string | null
  /** Antwort des N-ten qaf_comparison-Selects (1-basiert) — Fall 5 lässt
   * den ZWEITEN (den des inneren replaceComparisonFile) scheitern. */
  failComparisonSelectAt?: number
  copyError?: string
}

function makeMocks(cfg: MockConfig) {
  let comparisonSelects = 0
  const comparisonRow = {
    id: VALID_UUID,
    project_id: PROJECT_ID,
    part_number: 'P-1',
    baseline_file_id: cfg.baselineFileId,
    comparison_file_id: 'file-neu',
    comparison_mode: cfg.comparisonMode,
    engine_version: null,
  }
  const fileRow = { id: 'file-alt', storage_key: cfg.storageKey, original_file_name: 'original.xlsx' }

  const supabase = {
    auth: { getClaims: vi.fn(() => Promise.resolve({ data: { claims: { sub: 'user-1' } }, error: null })) },
    from: vi.fn((table: string) => {
      if (table === 'qaf_comparison') {
        comparisonSelects += 1
        const fail = cfg.failComparisonSelectAt === comparisonSelects
        return {
          select: vi.fn(() => ({
            eq: vi.fn(() => ({
              maybeSingle: vi.fn(() =>
                Promise.resolve(fail ? { data: null, error: { message: 'induced failure' } } : { data: comparisonRow, error: null }),
              ),
            })),
          })),
        }
      }
      if (table === 'qaf_file') {
        return {
          select: vi.fn(() => ({
            eq: vi.fn(() => ({
              maybeSingle: vi.fn(() => Promise.resolve({ data: fileRow, error: null })),
            })),
          })),
        }
      }
      throw new Error(`unexpected table queried: ${table}`)
    }),
  }

  const copy = vi.fn(() => Promise.resolve({ error: cfg.copyError ? { message: cfg.copyError } : null }))
  const remove = vi.fn(() => Promise.resolve({ error: null }))
  const admin = { storage: { from: vi.fn(() => ({ copy, remove })) } }

  return { supabase, admin, copy, remove }
}

let current = makeMocks({ comparisonMode: 'summary', storageKey: SOURCE_KEY, baselineFileId: 'file-alt' })

vi.mock('@/lib/supabase/server', () => ({
  createClient: vi.fn(() => Promise.resolve(current.supabase)),
}))
vi.mock('@/lib/supabase/admin', () => ({
  createAdminClient: vi.fn(() => current.admin),
}))

beforeEach(() => {
  vi.clearAllMocks()
})

async function reparse(role: 'alt' | 'neu' = 'alt') {
  const mod = await import('../file-replace-actions')
  return mod.reparseComparisonFile(VALID_UUID, role)
}

describe('reparseComparisonFile', () => {
  it('rejects non-summary modes by name (deferred boundary) without touching storage', async () => {
    current = makeMocks({ comparisonMode: 'multi_qaf', storageKey: SOURCE_KEY, baselineFileId: 'file-alt' })
    const res = await reparse()
    expect(res.ok).toBe(false)
    if (!res.ok) expect(res.error).toContain('nur für Summary')
    expect(current.copy).not.toHaveBeenCalled()
    expect(current.remove).not.toHaveBeenCalled()
  })

  it('names the missing legacy source instead of failing silently', async () => {
    current = makeMocks({ comparisonMode: 'summary', storageKey: null, baselineFileId: 'file-alt' })
    const res = await reparse()
    expect(res.ok).toBe(false)
    if (!res.ok) expect(res.error).toContain('Originalquelle nicht hinterlegt')
    expect(current.copy).not.toHaveBeenCalled()
  })

  it('counter-probe: a storage_key outside the project prefix is refused before any storage access', async () => {
    current = makeMocks({
      comparisonMode: 'summary',
      storageKey: 'fremdes-projekt/44444444-4444-4444-8444-444444444444-original.xlsx',
      baselineFileId: 'file-alt',
    })
    const res = await reparse()
    expect(res.ok).toBe(false)
    if (!res.ok) expect(res.error).toContain('gehört nicht zu diesem Projekt')
    expect(current.copy).not.toHaveBeenCalled()
    expect(current.remove).not.toHaveBeenCalled()
  })

  it('a failed bucket copy reports by name and never removes anything', async () => {
    current = makeMocks({
      comparisonMode: 'summary',
      storageKey: SOURCE_KEY,
      baselineFileId: 'file-alt',
      copyError: 'quota exceeded',
    })
    const res = await reparse()
    expect(res.ok).toBe(false)
    if (!res.ok) expect(res.error).toContain('Original nicht kopierbar')
    expect(current.remove).not.toHaveBeenCalled()
  })

  // Deckt den FRÜHEN Abbruch des inneren replace (vor dessen compensate-
  // Pfad): genau dann räumt NUR der Wrapper. Der Doppel-remove-Fall
  // (compensate + Wrapper auf derselben Kopie) bräuchte einen vollen
  // ingest-Mock (storage.list/download + alle Kind-Tabellen) — der
  // Original-Schutz dort ist durch Konstruktion gegeben (compensate
  // entfernt ausschließlich upload.path = Kopie, nie file.storage_key)
  // und als replaceComparisonFile-Testlücke in program-status benannt.
  it('when the inner replace fails early, only the COPY is cleaned up — the original is never removed', async () => {
    current = makeMocks({
      comparisonMode: 'summary',
      storageKey: SOURCE_KEY,
      baselineFileId: 'file-alt',
      failComparisonSelectAt: 2,
    })
    const res = await reparse()
    expect(res.ok).toBe(false)
    expect(current.copy).toHaveBeenCalledTimes(1)
    const [copyFrom, copyTo] = current.copy.mock.calls[0] as unknown as [string, string]
    expect(copyFrom).toBe(SOURCE_KEY)
    expect(copyTo).not.toBe(SOURCE_KEY)
    expect(copyTo.startsWith(`${PROJECT_ID}/`)).toBe(true)
    // Jeder remove-Aufruf (Wrapper-Nachräumen und/oder compensate) trifft
    // ausschließlich die Kopie — NIE das Original.
    expect(current.remove).toHaveBeenCalled()
    for (const call of current.remove.mock.calls) {
      const [paths] = call as unknown as [string[]]
      expect(paths).toEqual([copyTo])
      expect(paths).not.toContain(SOURCE_KEY)
    }
  })
})
