/**
 * FEAT-4: Tests für den Ahnenbaum-Builder (build.ts).
 *
 * Reine Daten-Tests (keine DOM/React): In-Memory-PedigreeSource mit
 * Aufruf-Zählern statt API.
 */
import { describe, expect, it } from 'vitest'
import type { Gerbil, Litter } from '../../api/types'
import {
  ancestorsAt,
  buildPedigree,
  collectNodes,
  expandPedigree,
  toRawNodeDatum,
} from '../build'
import type { AnimalNode, PedigreeNode, PedigreeSource } from '../types'

/* ── Testdaten-Helfer ─────────────────────────────────────────── */

function makeGerbil(id: string, name: string, litterId: string | null = null): Gerbil {
  return {
    id,
    name,
    gender: 'unknown',
    status: 'Active',
    dateOfBirth: null,
    dateOfDeath: null,
    causeOfDeath: null,
    goHomeDate: null,
    litterId,
    enclosureId: null,
    colorVarietyId: null,
    originContactId: null,
    receiverContactId: null,
    genotype: null,
    notes: null,
  }
}

function makeLitter(id: string, fatherId: string | null, motherId: string | null): Litter {
  return {
    id,
    name: `Wurf ${id}`,
    date: '2025-01-01',
    totalBorn: null,
    expectedGoHomeDate: null,
    notes: null,
    fatherId,
    motherId,
  }
}

interface CountingSource extends PedigreeSource {
  gerbilCalls: Map<string, number>
  litterCalls: Map<string, number>
}

function makeSource(gerbils: Gerbil[], litters: Litter[]): CountingSource {
  const gerbilMap = new Map(gerbils.map((g) => [g.id, g]))
  const litterMap = new Map(litters.map((l) => [l.id, l]))
  const gerbilCalls = new Map<string, number>()
  const litterCalls = new Map<string, number>()
  return {
    gerbilCalls,
    litterCalls,
    getGerbil: (id) => {
      gerbilCalls.set(id, (gerbilCalls.get(id) ?? 0) + 1)
      return Promise.resolve(gerbilMap.get(id) ?? null)
    },
    getLitter: (id) => {
      litterCalls.set(id, (litterCalls.get(id) ?? 0) + 1)
      return Promise.resolve(litterMap.get(id) ?? null)
    },
  }
}

/**
 * Standard-Familie:
 *   kind ← (vater, mutter); vater ← (opa, oma); mutter ohne Wurf.
 *   opa hat einen Wurf (uropas), der NICHT vorab geladen werden soll,
 *   wenn die Tiefe es begrenzt.
 */
function familySource(): CountingSource {
  return makeSource(
    [
      makeGerbil('kind', 'Kind', 'wurf-kind'),
      makeGerbil('vater', 'Vater', 'wurf-vater'),
      makeGerbil('mutter', 'Mutter', null),
      makeGerbil('opa', 'Opa', 'wurf-opa'),
      makeGerbil('oma', 'Oma', null),
      makeGerbil('uropa', 'Uropa', null),
    ],
    [
      makeLitter('wurf-kind', 'vater', 'mutter'),
      makeLitter('wurf-vater', 'opa', 'oma'),
      makeLitter('wurf-opa', 'uropa', null),
    ],
  )
}

function animal(node: PedigreeNode | undefined): AnimalNode {
  if (!node || node.kind !== 'animal') throw new Error(`Tier-Knoten erwartet, war: ${node?.kind}`)
  return node
}

/* ── buildPedigree ────────────────────────────────────────────── */

describe('buildPedigree', () => {
  it('baut den Baum mit Pfaden, Vater-vor-Mutter und Platzhaltern für Gründertiere', async () => {
    const root = await buildPedigree(familySource(), 'kind')
    expect(root).not.toBeNull()
    expect(root!.gerbil.name).toBe('Kind')
    expect(root!.path).toBe('')

    const vater = animal(root!.father)
    const mutter = animal(root!.mother)
    expect(vater.gerbil.name).toBe('Vater')
    expect(vater.path).toBe('v')
    expect(mutter.gerbil.name).toBe('Mutter')
    expect(mutter.path).toBe('m')

    // Mutter ohne Wurf: beide Eltern als „unbekannt“-Platzhalter (Symmetrie).
    expect(mutter.father).toEqual({ kind: 'unknown', path: 'mv' })
    expect(mutter.mother).toEqual({ kind: 'unknown', path: 'mm' })
    expect(mutter.expandable).toBe(false)
  })

  it('liefert null, wenn das Wurzeltier nicht existiert', async () => {
    expect(await buildPedigree(familySource(), 'gibt-es-nicht')).toBeNull()
  })

  it('begrenzt die Tiefe und markiert die Ladegrenze als erweiterbar', async () => {
    const root = await buildPedigree(familySource(), 'kind', 1)
    const vater = animal(root!.father)
    const mutter = animal(root!.mother)

    // Generation 1 ist die Grenze: keine Eltern geladen.
    expect(vater.father).toBeUndefined()
    expect(vater.mother).toBeUndefined()
    // Vater hat einen Wurf → nachladbar; Mutter ohne Wurf → nicht.
    expect(vater.expandable).toBe(true)
    expect(mutter.expandable).toBe(false)
  })

  it('Standardtiefe 4: lädt genau 4 Generationen', async () => {
    const root = await buildPedigree(familySource(), 'kind')
    // Uropa liegt in Generation 3 (kind → vater → opa → uropa) und hat keinen
    // Wurf → seine Eltern sind als Platzhalter geladen (Generation 4 = Grenze).
    const uropa = animal(animal(animal(root!.father).father).father)
    expect(uropa.gerbil.name).toBe('Uropa')
    expect(uropa.father).toEqual({ kind: 'unknown', path: 'vvvv' })
  })

  it('setzt Platzhalter für fehlenden Eltern-Eintrag im Wurf und für 404-Tiere', async () => {
    const source = makeSource(
      [makeGerbil('a', 'A', 'wurf-a')],
      // Vater-Id zeigt ins Leere (gelöschtes Tier), Mutter fehlt im Wurf.
      [makeLitter('wurf-a', 'geloescht', null)],
    )
    const root = await buildPedigree(source, 'a')
    expect(root!.father).toEqual({ kind: 'unknown', path: 'v' })
    expect(root!.mother).toEqual({ kind: 'unknown', path: 'm' })
  })

  it('behandelt einen 404-Wurf wie unbekannte Eltern', async () => {
    const source = makeSource([makeGerbil('a', 'A', 'wurf-weg')], [])
    const root = await buildPedigree(source, 'a')
    expect(root!.father).toEqual({ kind: 'unknown', path: 'v' })
    expect(root!.mother).toEqual({ kind: 'unknown', path: 'm' })
  })

  it('beendet Zyklen (Tier als eigener Vorfahre) ohne Endlosschleife', async () => {
    const source = makeSource(
      [makeGerbil('a', 'A', 'wurf-a'), makeGerbil('b', 'B', 'wurf-b')],
      [makeLitter('wurf-a', 'b', null), makeLitter('wurf-b', 'a', null)],
    )
    const root = await buildPedigree(source, 'a')
    const b = animal(root!.father)
    const aAgain = animal(b.father)
    expect(aAgain.gerbil.id).toBe('a')
    // Zyklus: keine Eltern, nicht erweiterbar (trotz vorhandenem Wurf).
    expect(aAgain.father).toBeUndefined()
    expect(aAgain.expandable).toBe(false)
  })

  it('dasselbe Tier darf an mehreren Positionen stehen (Inzucht ist kein Zyklus)', async () => {
    // Vater und Mutter haben denselben Vater (Opa) — Ahnenschwund, kein Zyklus.
    const source = makeSource(
      [
        makeGerbil('kind', 'Kind', 'w1'),
        makeGerbil('vater', 'Vater', 'w2'),
        makeGerbil('mutter', 'Mutter', 'w3'),
        makeGerbil('opa', 'Opa', null),
      ],
      [makeLitter('w1', 'vater', 'mutter'), makeLitter('w2', 'opa', null), makeLitter('w3', 'opa', null)],
    )
    const root = await buildPedigree(source, 'kind')
    expect(animal(animal(root!.father).father).gerbil.id).toBe('opa')
    expect(animal(animal(root!.mother).father).gerbil.id).toBe('opa')
  })
})

/* ── expandPedigree ───────────────────────────────────────────── */

describe('expandPedigree', () => {
  it('lädt an der Grenze eine weitere Generation nach und teilt unveränderte Äste', async () => {
    const source = familySource()
    const root = await buildPedigree(source, 'kind', 1)
    const vorher = animal(root!.father)
    expect(vorher.father).toBeUndefined()

    const next = await expandPedigree(source, root!, 'v')
    // Neuer Baum: erweiterter Ast hat jetzt Eltern …
    const vater = animal(next.father)
    expect(animal(vater.father).gerbil.name).toBe('Opa')
    expect(animal(vater.mother).gerbil.name).toBe('Oma')
    // … Wurzel ist ein neues Objekt, der unberührte Mutter-Ast wird geteilt.
    expect(next).not.toBe(root)
    expect(next.mother).toBe(root!.mother)
  })

  it('erweiterte Knoten an der neuen Grenze sind wiederum erweiterbar', async () => {
    const source = familySource()
    const root = await buildPedigree(source, 'kind', 1)
    const next = await expandPedigree(source, root!, 'v')
    // Opa (Generation 2, neue Grenze) hat einen Wurf → erweiterbar.
    expect(animal(animal(next.father).father).expandable).toBe(true)
  })
})

/* ── Caching-Vertrag der Quelle ───────────────────────────────── */

describe('Quellen-Aufrufe', () => {
  it('fragt jedes Tier und jeden Wurf höchstens einmal pro Aufbau an', async () => {
    const source = familySource()
    await buildPedigree(source, 'kind')
    for (const [id, count] of source.gerbilCalls) {
      expect(count, `Tier ${id} mehrfach geladen`).toBe(1)
    }
    for (const [id, count] of source.litterCalls) {
      expect(count, `Wurf ${id} mehrfach geladen`).toBe(1)
    }
  })
})

/* ── toRawNodeDatum / collectNodes ────────────────────────────── */

describe('toRawNodeDatum', () => {
  it('konvertiert in die react-d3-tree-Struktur (Name, Pfad, Kinder [Vater, Mutter])', async () => {
    const root = await buildPedigree(familySource(), 'kind', 1)
    const datum = toRawNodeDatum(root!, 'unbekannt')

    expect(datum.name).toBe('Kind')
    expect(datum.attributes).toMatchObject({ path: '', kind: 'animal', expandable: false })
    expect(datum.children).toHaveLength(2)
    expect(datum.children![0].name).toBe('Vater')
    expect(datum.children![0].attributes).toMatchObject({ path: 'v', expandable: true })
    expect(datum.children![1].name).toBe('Mutter')
    // Grenz-Knoten: keine children-Eigenschaft (nicht geladen ≠ leer).
    expect(datum.children![0].children).toBeUndefined()
  })

  it('rendert Platzhalter mit dem übergebenen Label', async () => {
    const root = await buildPedigree(familySource(), 'mutter')
    const datum = toRawNodeDatum(root!, 'unbekannt')
    expect(datum.children![0]).toEqual({
      name: 'unbekannt',
      attributes: { path: 'v', kind: 'unknown' },
    })
  })

  it('collectNodes indiziert jeden Knoten unter seinem Pfad', async () => {
    const root = await buildPedigree(familySource(), 'kind')
    const nodes = collectNodes(root!)
    expect(nodes.get('')).toBe(root)
    expect(animal(nodes.get('vv')).gerbil.name).toBe('Opa')
    expect(nodes.get('mv')?.kind).toBe('unknown')
  })
})

/* ── ancestorsAt (Druckansicht) ───────────────────────────────── */

describe('ancestorsAt', () => {
  it('liefert die Plätze einer Generation in Ahnentafel-Ordnung mit Lücken', async () => {
    const root = await buildPedigree(familySource(), 'kind')

    expect(ancestorsAt(root!, 0)).toEqual([root])

    const gen1 = ancestorsAt(root!, 1)
    expect(gen1.map((n) => (n?.kind === 'animal' ? n.gerbil.name : n?.kind))).toEqual([
      'Vater',
      'Mutter',
    ])

    const gen2 = ancestorsAt(root!, 2)
    expect(gen2).toHaveLength(4)
    expect(animal(gen2[0]!).gerbil.name).toBe('Opa')
    expect(animal(gen2[1]!).gerbil.name).toBe('Oma')
    expect(gen2[2]?.kind).toBe('unknown') // Mutter-Seite: Platzhalter
    expect(gen2[3]?.kind).toBe('unknown')

    const gen3 = ancestorsAt(root!, 3)
    expect(gen3).toHaveLength(8)
    expect(animal(gen3[0]!).gerbil.name).toBe('Uropa')
    expect(gen3[1]?.kind).toBe('unknown') // Opas Wurf ohne Mutter
    // Hinter Oma (ohne Wurf, Platzhalter-Eltern) …
    expect(gen3[2]?.kind).toBe('unknown')
    // … hinter Platzhaltern: leere Plätze.
    expect(gen3.slice(4)).toEqual([null, null, null, null])
  })
})
