import { describe, it, expect, vi, beforeEach } from 'vitest'
import { persistSimvsmImport } from '../persist'
import type { MappedStream } from '../streams'
import type { VsmNode } from '@/lib/vsm-types'

function makeNode(overrides: Partial<VsmNode> = {}): VsmNode {
  return { id: overrides.id ?? 'n1', type: 'process', x: 0, y: 0, name: 'Testprozess', provenance: 'imported', ...overrides }
}

function makeStream(overrides: Partial<MappedStream> = {}): MappedStream {
  return {
    index: 0,
    name: 'Ist-Zustand',
    isMainFlag: false,
    scenarioRole: 'current',
    hasResultData: false,
    nodes: [makeNode()],
    connections: [],
    nodeMappings: [],
    linkMappings: [],
    ...overrides,
  }
}

function makeSupabaseMock(
  opts: {
    idempotencyResult?: { data: unknown; error: null | { message: string } }
    insertResults?: Array<{ data: unknown; error: null | { message: string } }>
  } = {},
) {
  const idempotencyResult = opts.idempotencyResult ?? { data: [], error: null }
  const insertResults = opts.insertResults ?? [{ data: { id: 'vsm-1' }, error: null }, { data: { id: 'vsm-2' }, error: null }, { data: { id: 'vsm-3' }, error: null }]
  let insertCallIndex = 0
  const recordedInserts: Record<string, unknown>[] = []

  function makeChain() {
    const chain: Record<string, unknown> = {}
    chain.select = vi.fn(() => chain)
    chain.eq = vi.fn(() => Promise.resolve(idempotencyResult))
    chain.single = vi.fn(() => {
      const result = insertResults[Math.min(insertCallIndex, insertResults.length - 1)]
      insertCallIndex++
      return Promise.resolve(result)
    })
    chain.insert = vi.fn((row: Record<string, unknown>) => {
      recordedInserts.push(row)
      return chain
    })
    return chain
  }

  return { from: vi.fn(() => makeChain()), _recordedInserts: recordedInserts }
}

let supabase = makeSupabaseMock()

beforeEach(() => {
  supabase = makeSupabaseMock()
})

const BASE_INPUT = {
  sourceSignature: 'source-sig-abc',
  fileName: 'werk-nord.json',
  modelName: 'Werk Nord Testmodell',
  createdWithVersion: '3.36.0',
  mainVersion: '3.20.0',
  createdBy: 'user-1',
}

describe('persistSimvsmImport', () => {
  it('rejects with no_stream_selected when streamIndices is empty', async () => {
    const outcome = await persistSimvsmImport(supabase as never, {
      ...BASE_INPUT,
      allStreams: [makeStream()],
      selection: { streamIndices: [], projectId: null },
    })
    expect(outcome).toEqual({ ok: false, error: 'no_stream_selected' })
    expect(supabase.from).not.toHaveBeenCalled()
  })

  it('rejects with invalid_stream_index when an index is out of range', async () => {
    const outcome = await persistSimvsmImport(supabase as never, {
      ...BASE_INPUT,
      allStreams: [makeStream()],
      selection: { streamIndices: [5], projectId: null },
    })
    expect(outcome).toEqual({ ok: false, error: 'invalid_stream_index' })
  })

  it('happy path: current + alternative selected → current inserted first, alternative parented to it', async () => {
    const current = makeStream({ index: 0, name: 'Ist-Zustand', scenarioRole: 'current', nodes: [makeNode({ id: 'a' })] })
    const alt = makeStream({ index: 1, name: 'Alternative A', scenarioRole: 'alternative', nodes: [makeNode({ id: 'b' })] })
    const outcome = await persistSimvsmImport(supabase as never, {
      ...BASE_INPUT,
      allStreams: [current, alt],
      selection: { streamIndices: [0, 1], projectId: 'proj-1' },
    })
    expect(outcome.ok).toBe(true)
    if (!outcome.ok) return
    expect(outcome.currentValueStreamId).toBe('vsm-1')
    expect(outcome.createdStreams).toHaveLength(2)

    const [firstRow, secondRow] = supabase._recordedInserts
    expect(firstRow.scenario_kind).toBe('current')
    expect(firstRow.parent_value_stream_id).toBeNull()
    expect(secondRow.scenario_kind).toBe('alternative')
    expect(secondRow.parent_value_stream_id).toBe('vsm-1')
  })

  it('selecting ONLY an alternative (current not selected) makes it a standalone current with no parent', async () => {
    const current = makeStream({ index: 0, name: 'Ist-Zustand', scenarioRole: 'current' })
    const alt = makeStream({ index: 1, name: 'Alternative A', scenarioRole: 'alternative' })
    const outcome = await persistSimvsmImport(supabase as never, {
      ...BASE_INPUT,
      allStreams: [current, alt],
      selection: { streamIndices: [1], projectId: null },
    })
    expect(outcome.ok).toBe(true)
    const [row] = supabase._recordedInserts
    expect(row.scenario_kind).toBe('current')
    expect(row.parent_value_stream_id).toBeNull()
  })

  it('two alternatives selected without the current stream both become independent standalone current rows', async () => {
    const current = makeStream({ index: 0, scenarioRole: 'current' })
    const altA = makeStream({ index: 1, name: 'Alternative A', scenarioRole: 'alternative' })
    const altB = makeStream({ index: 2, name: 'Alternative B', scenarioRole: 'alternative' })
    const outcome = await persistSimvsmImport(supabase as never, {
      ...BASE_INPUT,
      allStreams: [current, altA, altB],
      selection: { streamIndices: [1, 2], projectId: null },
    })
    expect(outcome.ok).toBe(true)
    expect(supabase._recordedInserts.every((r) => r.scenario_kind === 'current' && r.parent_value_stream_id === null)).toBe(true)
  })

  it('title has no suffix for a single-stream selection out of a single-alternative file', async () => {
    const outcome = await persistSimvsmImport(supabase as never, {
      ...BASE_INPUT,
      allStreams: [makeStream({ index: 0, name: 'Ist-Zustand' })],
      selection: { streamIndices: [0], projectId: null },
    })
    expect(outcome.ok).toBe(true)
    expect(supabase._recordedInserts[0].title).toBe('Werk Nord Testmodell')
  })

  it('title gets a " · <StreamName>" suffix when the file has more than one alternative', async () => {
    const current = makeStream({ index: 0, name: 'Ist-Zustand' })
    const alt = makeStream({ index: 1, name: 'Alternative A', scenarioRole: 'alternative' })
    await persistSimvsmImport(supabase as never, {
      ...BASE_INPUT,
      allStreams: [current, alt],
      selection: { streamIndices: [0, 1], projectId: null },
    })
    expect(supabase._recordedInserts[0].title).toBe('Werk Nord Testmodell · Ist-Zustand')
    expect(supabase._recordedInserts[1].title).toBe('Werk Nord Testmodell · Alternative A')
  })

  it('createdStreams carries the actual DB row title (§14.4 "imported value streams" — C8), not just the raw SimVSM stream name', async () => {
    const current = makeStream({ index: 0, name: 'Ist-Zustand' })
    const alt = makeStream({ index: 1, name: 'Alternative A', scenarioRole: 'alternative' })
    const outcome = await persistSimvsmImport(supabase as never, {
      ...BASE_INPUT,
      allStreams: [current, alt],
      selection: { streamIndices: [0, 1], projectId: null },
    })
    expect(outcome.ok).toBe(true)
    if (!outcome.ok) return
    expect(outcome.createdStreams[0]).toMatchObject({ streamName: 'Ist-Zustand', title: 'Werk Nord Testmodell · Ist-Zustand', scenarioKind: 'current' })
    expect(outcome.createdStreams[1]).toMatchObject({ streamName: 'Alternative A', title: 'Werk Nord Testmodell · Alternative A', scenarioKind: 'alternative' })
  })

  it('respects an explicit titleOverride', async () => {
    await persistSimvsmImport(supabase as never, {
      ...BASE_INPUT,
      allStreams: [makeStream()],
      selection: { streamIndices: [0], projectId: null, titleOverride: 'Mein eigener Titel' },
    })
    expect(supabase._recordedInserts[0].title).toBe('Mein eigener Titel')
  })

  it('sets is_demo:false, created_by, and provenance-carrying nodes on every inserted row', async () => {
    await persistSimvsmImport(supabase as never, {
      ...BASE_INPUT,
      allStreams: [makeStream()],
      selection: { streamIndices: [0], projectId: null },
    })
    const row = supabase._recordedInserts[0]
    expect(row.is_demo).toBe(false)
    expect(row.created_by).toBe('user-1')
    expect((row.nodes as VsmNode[])[0].provenance).toBe('imported')
  })

  it('writes source metadata into layout.simvsmSource (no schema/migration touch)', async () => {
    await persistSimvsmImport(supabase as never, {
      ...BASE_INPUT,
      allStreams: [makeStream({ index: 0, name: 'Ist-Zustand' })],
      selection: { streamIndices: [0], projectId: null },
    })
    const layout = supabase._recordedInserts[0].layout as { simvsmSource: Record<string, unknown> }
    expect(layout.simvsmSource).toMatchObject({
      sourceSignature: 'source-sig-abc',
      fileName: 'werk-nord.json',
      modelName: 'Werk Nord Testmodell',
      streamIndex: 0,
      streamName: 'Ist-Zustand',
    })
    expect(typeof layout.simvsmSource.importSignature).toBe('string')
  })

  it('idempotent re-confirm: an existing importSignature match returns the prior result with zero inserts', async () => {
    supabase = makeSupabaseMock({
      idempotencyResult: {
        data: [
          { id: 'vsm-existing-1', title: 'Werk Nord Testmodell', scenario_kind: 'current', parent_value_stream_id: null },
        ],
        error: null,
      },
    })
    const outcome = await persistSimvsmImport(supabase as never, {
      ...BASE_INPUT,
      allStreams: [makeStream()],
      selection: { streamIndices: [0], projectId: null },
    })
    expect(outcome.ok).toBe(true)
    if (!outcome.ok) return
    expect(outcome.idempotentHit).toBe(true)
    expect(outcome.currentValueStreamId).toBe('vsm-existing-1')
    expect(supabase._recordedInserts).toHaveLength(0)
  })

  it('anchor (current) insert failure aborts the whole confirm — nothing else is inserted', async () => {
    supabase = makeSupabaseMock({ insertResults: [{ data: null, error: { message: 'constraint violation' } }] })
    const current = makeStream({ index: 0 })
    const alt = makeStream({ index: 1, name: 'Alternative A', scenarioRole: 'alternative' })
    const outcome = await persistSimvsmImport(supabase as never, {
      ...BASE_INPUT,
      allStreams: [current, alt],
      selection: { streamIndices: [0, 1], projectId: null },
    })
    expect(outcome).toEqual({ ok: false, error: 'db_error' })
    expect(supabase._recordedInserts).toHaveLength(1)
  })

  it('a later (non-anchor) insert failure is reported per-stream, current row stays created', async () => {
    supabase = makeSupabaseMock({
      insertResults: [{ data: { id: 'vsm-1' }, error: null }, { data: null, error: { message: 'constraint violation' } }],
    })
    const current = makeStream({ index: 0 })
    const alt = makeStream({ index: 1, name: 'Alternative A', scenarioRole: 'alternative' })
    const outcome = await persistSimvsmImport(supabase as never, {
      ...BASE_INPUT,
      allStreams: [current, alt],
      selection: { streamIndices: [0, 1], projectId: null },
    })
    expect(outcome.ok).toBe(true)
    if (!outcome.ok) return
    expect(outcome.currentValueStreamId).toBe('vsm-1')
    expect(outcome.createdStreams.find((s) => s.streamIndex === 0)?.ok).toBe(true)
    expect(outcome.createdStreams.find((s) => s.streamIndex === 1)?.ok).toBe(false)
  })

  it('rejects with mapper_output_invalid (no insert attempted) when a stream node fails schema validation', async () => {
    const invalidStream = makeStream({ index: 0, nodes: [makeNode({ availabilityPct: 250 })] })
    const outcome = await persistSimvsmImport(supabase as never, {
      ...BASE_INPUT,
      allStreams: [invalidStream],
      selection: { streamIndices: [0], projectId: null },
    })
    expect(outcome).toEqual({ ok: false, error: 'mapper_output_invalid' })
    expect(supabase._recordedInserts).toHaveLength(0)
  })
})
