/**
 * Test de NON-PERTE du seed Ğ1 — BLUEPRINT-V2.md « Seeds » (bloquant).
 *
 * Vérifie que frontend/app/data/seeds/duniter-g1.bundle.json (émis par
 * backend/scripts/export_seed_bundle.py depuis backend/seed.py) conserve
 * l'intégralité du contenu réel Ğ1 :
 *   - 33 clauses Certification + 59 clauses Forgeron, exactement ;
 *   - presets d'inertie conservés (les 4 valeurs présentes sur chaque doc,
 *     répartition exacte du seed v1 : cert 8/19/4/2, forgeron 4/49/4/2) ;
 *   - provenance.voteRecord == { for: 97, against: 23, invalid: 19,
 *     wotSize: 7224, thresholdRequired: 97 }, modeParams 'D30M50B.1G.2S.1' ;
 *   - 11 personnes dont 5 smiths ; 3 mandats ; ≥ 7 protocoles dont le
 *     Consentement (invariant) ;
 *   - chaque clause courante chaînée à sa décision fondatrice adoptée ;
 *   - chaque entité porte collectiveId / createdAt / updatedAt.
 */
import { describe, expect, it } from 'vitest'
import { resolveSettings } from '~/engine/settings'
import type {
  Bundle,
  Clause,
  ClauseVersion,
  Decision,
  InertiaPreset,
  Mandate,
  Person,
  Protocol,
  TextDoc,
  Vote,
  VoteSession,
} from '~/types/domain'
import raw from '../../app/data/seeds/duniter-g1.bundle.json'

const bundle = raw as unknown as Bundle

const docBySlug = (slug: string): TextDoc => {
  const doc = bundle.docs.find((d) => d.slug === slug)
  expect(doc, `doc ${slug}`).toBeDefined()
  return doc as TextDoc
}
const clausesOf = (doc: TextDoc): Clause[] => bundle.clauses.filter((c) => c.docId === doc.id)
const versionById = new Map<string, ClauseVersion>(bundle.versions.map((v) => [v.id, v]))
const decisionById = new Map<string, Decision>(bundle.decisions.map((d) => [d.id, d]))
const protocolById = new Map<string, Protocol>(bundle.protocols.map((p) => [p.id, p]))
const personByName = new Map<string, Person>(bundle.people.map((p) => [p.displayName, p]))

const inertiaDistribution = (doc: TextDoc): Record<string, number> => {
  const dist: Record<string, number> = {}
  for (const clause of clausesOf(doc)) dist[clause.inertia] = (dist[clause.inertia] ?? 0) + 1
  return dist
}

describe('bundle duniter-g1 — schéma v2', () => {
  it('est un Bundle schemaVersion 2 avec assents:[]', () => {
    expect(bundle.schemaVersion).toBe(2)
    expect(bundle.exportedAt).toBeTruthy()
    expect(bundle.assents).toEqual([])
    expect(bundle.concerns).toEqual([])
    expect(bundle.objections).toEqual([])
    expect(bundle.advices).toEqual([])
  })

  it('collectif : slug/template/transparence/couleur/icône de la spec Seeds', () => {
    const c = bundle.collective
    expect(c.slug).toBe('duniter-g1')
    expect(c.template).toBe('free-currency')
    expect(c.isTransparent).toBe(true)
    expect(c.color).toBe('#22c55e')
    expect(c.icon).toBe('i-lucide-network')
    expect(bundle.docs.find((d) => d.id === c.pactDocId)?.role).toBe('pact')
    expect(bundle.circles.find((ci) => ci.id === c.rootCircleId)?.name).toBe('Toile de confiance')
  })

  it('chaque entité porte collectiveId, createdAt et updatedAt', () => {
    const collections = [
      bundle.people,
      bundle.circles,
      bundle.decisions,
      bundle.concerns,
      bundle.objections,
      bundle.advices,
      bundle.assents,
      bundle.mandates,
      bundle.docs,
      bundle.clauses,
      bundle.versions,
      bundle.protocols,
      bundle.sessions,
      bundle.votes,
    ] as { id: string; collectiveId: string; createdAt: string; updatedAt: string }[][]
    for (const collection of collections) {
      for (const item of collection) {
        expect(item.collectiveId, `collectiveId de ${item.id}`).toBe(bundle.collective.id)
        expect(item.createdAt, `createdAt de ${item.id}`).toBeTruthy()
        expect(item.updatedAt, `updatedAt de ${item.id}`).toBeTruthy()
      }
    }
    expect(bundle.collective.createdAt).toBeTruthy()
    expect(bundle.collective.updatedAt).toBeTruthy()
  })
})

describe('non-perte — Acte d’engagement Certification (33 items)', () => {
  const doc = docBySlug('engagement-certification')
  const clauses = clausesOf(doc)

  it('contient exactement 33 clauses aux codes uniques', () => {
    expect(clauses).toHaveLength(33)
    expect(new Set(clauses.map((c) => c.code)).size).toBe(33)
  })

  it('conserve la répartition des 4 presets d’inertie du seed v1 (8/19/4/2)', () => {
    expect(inertiaDistribution(doc)).toEqual({ low: 8, standard: 19, high: 4, max: 2 })
  })

  it('conserve le texte voté (échantillon E1 — unicité du compte)', () => {
    const e1 = clauses.find((c) => c.code === 'E1')
    expect(e1).toBeDefined()
    const version = versionById.get(e1!.currentVersionId!)
    expect(version?.content).toContain('un seul et unique compte cocréateur de monnaie G1')
  })

  it('porte la provenance v1 (sources + contributeurs)', () => {
    expect(doc.provenance?.sources.length).toBeGreaterThanOrEqual(6)
    expect(doc.provenance?.contributors?.join(' ')).toContain('1000i100')
  })
})

describe('non-perte — Acte d’engagement forgeron (59 items v2.0.0-fr)', () => {
  const doc = docBySlug('engagement-forgeron')
  const clauses = clausesOf(doc)

  it('contient exactement 59 clauses aux codes uniques, en versionLabel 2.0.0-fr', () => {
    expect(clauses).toHaveLength(59)
    expect(new Set(clauses.map((c) => c.code)).size).toBe(59)
    for (const clause of clauses) {
      expect(versionById.get(clause.currentVersionId!)?.versionLabel).toBe('2.0.0-fr')
    }
  })

  it('conserve les 18 clauses aspirant (A1..A18) et les 16 certificateur (C1..C16)', () => {
    const codes = new Set(clauses.map((c) => c.code))
    for (let i = 1; i <= 18; i++) expect(codes.has(`A${i}`), `A${i}`).toBe(true)
    for (let i = 1; i <= 16; i++) expect(codes.has(`C${i}`), `C${i}`).toBe(true)
  })

  it('conserve la répartition des 4 presets d’inertie du seed v1 (4/49/4/2)', () => {
    expect(inertiaDistribution(doc)).toEqual({ low: 4, standard: 49, high: 4, max: 2 })
  })

  it('voteRecord === {for:97, against:23, invalid:19, wotSize:7224, thresholdRequired:97}', () => {
    const record = doc.provenance?.voteRecord
    expect(record).toBeDefined()
    expect(record!.result).toMatchObject({
      for: 97,
      against: 23,
      invalid: 19,
      wotSize: 7224,
      thresholdRequired: 97,
    })
    expect(record!.modeParams).toBe('D30M50B.1G.2S.1')
    expect(record!.period).toBe('2026-01-07 → 2026-02-06')
    expect(record!.url).toBe(
      'https://forum.monnaie-libre.fr/t/vote-engagement-forgeron-v2-0-0/33165',
    )
  })
})

describe('non-perte — Comité Technique et Pacte', () => {
  it('engagement-comite-tech : clauses présentes, source du vote 33293 tracée', () => {
    const doc = docBySlug('engagement-comite-tech')
    const clauses = clausesOf(doc)
    expect(clauses.length).toBeGreaterThanOrEqual(4)
    expect(doc.provenance?.sources.some((s) => s.url.includes('33293'))).toBe(true)
    // Contenu reconstruit (vote en cours au moment de la recherche) — noté :
    expect(doc.provenance?.notes).toContain('research_duniter_forum.md')
  })

  it('pacte-duniter-g1 : rôle pact, description sacralisée, A1 « Notre finalité »', () => {
    const pact = docBySlug('pacte-duniter-g1')
    expect(pact.role).toBe('pact')
    expect(pact.description).toBe('Notre contrat social — sacralisé, jamais immuable')
    const a1 = clausesOf(pact).find((c) => c.code === 'A1')
    expect(a1?.title).toBe('Notre finalité')
    const content = versionById.get(a1!.currentVersionId!)?.content ?? ''
    for (const word of ['Autonomie', 'Équilibre', 'Liaison', 'toile de confiance Ğ1']) {
      expect(content).toContain(word)
    }
  })

  it('le Pacte résout les réglages v2 (resolveSettings) avec les seuils du seed', () => {
    const pact = docBySlug('pacte-duniter-g1')
    const settings = resolveSettings(clausesOf(pact), bundle.versions)
    expect(settings.triage).toEqual({
      smallGroupMax: 5,
      collectiveMin: 50,
      consentMax: 7,
      objectionWindowHours: 48,
      adviceWindowHours: 72,
      framingDays: 14,
      concernEscalateRatio: 0.5,
      recurrenceThreshold: 3,
      reviewDelayDays: 90,
      requireEffects: 'binding',
    })
    // consent OBLIGATOIRE + large = binaire inertiel (héritage free-currency)
    expect(protocolById.get(settings.protocolByRange.consent)?.method).toBe('consent')
    expect(protocolById.get(settings.protocolByRange.large!)?.name).toBe('Vote WoT standard')
    expect(protocolById.get(settings.protocolByRange.nuanced!)?.method).toBe('nuanced')
    expect(protocolById.get(settings.protocolByRange.parametric!)?.method).toBe('parametric')
    expect(protocolById.get(settings.protocolByRange.election!)?.method).toBe('election')
  })

  it('clauseByInertia : les 4 presets pointent les formules v1 (M/G intacts)', () => {
    const pact = docBySlug('pacte-duniter-g1')
    const settings = resolveSettings(clausesOf(pact), bundle.versions)
    const byInertia = settings.protocolByRange.clauseByInertia
    expect(byInertia).toBeDefined()
    const expected: Record<InertiaPreset, { M: number; G: number }> = {
      low: { M: 50, G: 0.1 },
      standard: { M: 50, G: 0.2 },
      high: { M: 60, G: 0.4 },
      max: { M: 66, G: 0.6 },
    }
    for (const preset of Object.keys(expected) as InertiaPreset[]) {
      const protocol = protocolById.get(byInertia![preset])
      expect(protocol, preset).toBeDefined()
      expect(protocol!.formula.majorityPct, preset).toBe(expected[preset].M)
      expect(protocol!.formula.gradientExponent, preset).toBe(expected[preset].G)
      expect(protocol!.formula.baseExponent, preset).toBe(0.1)
    }
    // La clause qui régit l'inertie est elle-même en preset 'max' (méta-protection)
    const metaClauses = clausesOf(pact).filter((c) =>
      c.settingKey?.startsWith('protocols.clauseByInertia.'),
    )
    expect(metaClauses.length).toBe(4)
    for (const clause of metaClauses) expect(clause.inertia, clause.code).toBe('max')
  })
})

describe('non-perte — personnes, protocoles, mandats', () => {
  it('11 personnes dont exactement 5 smiths', () => {
    expect(bundle.people).toHaveLength(11)
    expect(bundle.people.filter((p) => p.wotStatus === 'smith')).toHaveLength(5)
    for (const name of ['Moul', 'Poka', 'Hugo', 'Elois', 'Cgeek']) {
      expect(personByName.get(name)?.wotStatus, name).toBe('smith')
    }
  })

  it('≥ 7 protocoles, tous ballot open, avec le Consentement OBLIGATOIRE', () => {
    expect(bundle.protocols.length).toBeGreaterThanOrEqual(7)
    for (const p of bundle.protocols) expect(p.ballot, p.name).toBe('open')
    expect(bundle.protocols.filter((p) => p.method === 'consent')).toHaveLength(1)
    const methods = new Set(bundle.protocols.map((p) => p.method))
    for (const m of ['consent', 'binary', 'nuanced', 'parametric', 'election']) {
      expect(methods.has(m as Protocol['method']), m).toBe(true)
    }
  })

  it('formules v1 intactes : WoT standard, Smith S.1, ComTech T.1, nuancé 59/80', () => {
    const wot = bundle.protocols.find((p) => p.name === 'Vote WoT standard')!
    expect(wot.modeParams).toBe('D30M50B.1G.2')
    expect(wot.formula).toMatchObject({ majorityPct: 50, baseExponent: 0.1, gradientExponent: 0.2 })
    expect(wot.description).toContain('dernier recours des très grands corps')
    expect(wot.description).toContain('héritage Toile de Confiance')

    const smith = bundle.protocols.find((p) => p.name === 'Vote forgeron (Smith)')!
    expect(smith.modeParams).toBe('D30M50B.1G.2S.1')
    expect(smith.formula.smithExponent).toBe(0.1)

    const tech = bundle.protocols.find((p) => p.name === 'Vote Comité Technique')!
    expect(tech.modeParams).toBe('D30M50B.1G.2T.1')
    expect(tech.formula.techcommExponent).toBe(0.1)

    const nuanced = bundle.protocols.find((p) => p.method === 'nuanced')!
    expect(nuanced.durationDays).toBe(30)
    expect(nuanced.formula.nuancedMinParticipants).toBe(59)
    expect(nuanced.formula.nuancedThresholdPct).toBe(80)

    const parametric = bundle.protocols.find((p) => p.method === 'parametric')!
    expect(parametric.durationDays).toBe(14)
    expect(parametric.formula.parametricMinParticipants).toBe(5)

    const election = bundle.protocols.find((p) => p.method === 'election')!
    expect(election.durationDays).toBe(14)
    expect(election.formula.electionMinParticipants).toBe(5)
    expect(election.formula.tieBreak).toBe('runoff')
  })

  it('3 mandats : ComTech actif (Cgeek, 1 rapport dû) · Admin Forgerons proposé · Modération active', () => {
    expect(bundle.mandates).toHaveLength(3)

    const comtech = bundle.mandates.find((m) => m.title.includes('Comité Technique')) as Mandate
    expect(comtech.status).toBe('active')
    expect(comtech.holderId).toBe(personByName.get('Cgeek')!.id)
    const due = comtech.reports.filter((r) => !r.deliveredAt)
    expect(due.length).toBeGreaterThanOrEqual(1)

    const admin = bundle.mandates.find((m) => m.title.includes('Administrateur')) as Mandate
    expect(admin.status).toBe('proposed')

    const forum = bundle.mandates.find((m) => m.title.includes('Modération')) as Mandate
    expect(forum.status).toBe('active')
    expect(forum.domain.tags).toEqual(['forum', 'moderation'])
  })

  it('le mandat proposé est EN vote nuancé : session ouverte + 2-3 votes', () => {
    const admin = bundle.mandates.find((m) => m.title.includes('Administrateur')) as Mandate
    const origin = decisionById.get(admin.originDecisionId)!
    expect(origin.status).toBe('voting')
    expect(origin.createsMandate).toBeDefined()
    expect(protocolById.get(origin.protocolId!)?.method).toBe('nuanced')

    const session = bundle.sessions.find((s) => s.decisionId === origin.id) as VoteSession
    expect(session).toBeDefined()
    expect(session.status).toBe('open')
    expect(session.corpusSize).toBe(session.corpusPersonIds.length)

    const sessionVotes = bundle.votes.filter((v) => v.sessionId === session.id) as Vote[]
    expect(sessionVotes.length).toBeGreaterThanOrEqual(2)
    expect(sessionVotes.length).toBeLessThanOrEqual(3)
    for (const vote of sessionVotes) {
      expect(typeof vote.value).toBe('number')
      expect(bundle.people.some((p) => p.id === vote.voterId)).toBe(true)
    }
  })
})

describe('invariant — la décision est le pivot', () => {
  it('chaque clause courante a sa décision fondatrice adoptée, chaînée', () => {
    for (const clause of bundle.clauses) {
      expect(clause.currentVersionId, `currentVersionId de ${clause.code}`).toBeTruthy()
      const version = versionById.get(clause.currentVersionId!)!
      expect(version.status, `version de ${clause.code}`).toBe('current')
      const decision = decisionById.get(version.decisionId)!
      expect(decision, `décision fondatrice de ${clause.code}`).toBeDefined()
      expect(decision.status).toBe('adopted')
      expect(decision.route).toBe('collective')
      expect(decision.title).toBe(`Adoption fondatrice — ${clause.code}`)
      expect(decision.amendsClauseId).toBe(clause.id)
      expect(decision.decidedAt).toBeTruthy()
    }
  })

  it('décisions historiques : Runtime Upgrade 1100 gravée + licence v0.4.0 en formulation', () => {
    const runtime = bundle.decisions.find((d) => d.title === 'Runtime Upgrade 1100')!
    expect(runtime.status).toBe('adopted')
    expect(runtime.weight).toBe('structural')
    expect(runtime.engraving?.proofLevel).toBe('local')
    expect(runtime.engraving?.sha256).toMatch(/^[0-9a-f]{64}$/)
    expect(runtime.review?.dueAt).toBeTruthy()
    expect(runtime.brief?.effects.length).toBeGreaterThanOrEqual(1)
    expect(runtime.brief?.effects.some((e) => e.target && e.measured)).toBe(true)

    const licence = bundle.decisions.find((d) => d.title === 'Évolution Licence G1 v0.4.0')!
    expect(licence.status).toBe('framing')
    const proposed = bundle.versions.filter(
      (v) => v.decisionId === licence.id && v.status === 'proposed',
    )
    expect(proposed.length).toBeGreaterThanOrEqual(1)
    const certDoc = docBySlug('engagement-certification')
    const certClauseIds = new Set(clausesOf(certDoc).map((c) => c.id))
    for (const version of proposed) {
      expect(certClauseIds.has(version.clauseId), 'version proposée liée à une clause cert').toBe(true)
    }
  })
})
