/**
 * resolveSettings — the Pact IS the settings store (Δ10).
 * Defaults, voted overrides, specified fallbacks, corrupted-bundle resilience.
 */
import { describe, expect, it } from 'vitest'
import { SETTINGS_DEFAULTS, hasConsentProtocol, resolveSettings } from '../../app/engine/settings'
import type { Clause, ClauseVersion, Json } from '../../app/types/domain'

const T0 = '2026-08-01T00:00:00.000Z'
const T1 = '2026-08-05T00:00:00.000Z'

let seq = 0

function makeClause(settingKey: string, overrides: Partial<Clause> = {}): Clause {
  seq += 1
  return {
    id: `clause-${seq}`,
    collectiveId: 'col-1',
    createdAt: T0,
    updatedAt: T0,
    docId: 'doc-pact',
    section: 'Réglages',
    position: seq,
    code: `S${seq}`,
    title: settingKey,
    inertia: 'standard',
    settingKey,
    ...overrides,
  }
}

function makeVersion(
  clauseId: string,
  settingValue: Json,
  overrides: Partial<ClauseVersion> = {},
): ClauseVersion {
  seq += 1
  return {
    id: `version-${seq}`,
    collectiveId: 'col-1',
    createdAt: T0,
    updatedAt: T0,
    clauseId,
    decisionId: 'dec-founding',
    versionLabel: 'v1',
    content: 'clause adoptée',
    settingValue,
    status: 'current',
    adoptedAt: T0,
    ...overrides,
  }
}

/** Build a Pact from [settingKey, settingValue] pairs and resolve it. */
function resolveFrom(entries: [string, Json][]) {
  const clauses = entries.map(([key]) => makeClause(key))
  const versions = clauses.map((clause, index) => makeVersion(clause.id, entries[index]![1]))
  return resolveSettings(clauses, versions)
}

describe('resolveSettings — les défauts complets', () => {
  it('sans aucune clause, tous les seuils de triage prennent leur défaut', () => {
    const settings = resolveSettings([], [])
    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',
    })
    expect(settings.triage).toEqual(SETTINGS_DEFAULTS)
  })

  it('sans aucune clause, les protocoles optionnels restent indéfinis (repli → consent chez l’appelant)', () => {
    const settings = resolveSettings([], [])
    expect(settings.protocolByRange.nuanced).toBeUndefined()
    expect(settings.protocolByRange.large).toBeUndefined()
    expect(settings.protocolByRange.parametric).toBeUndefined()
    expect(settings.protocolByRange.election).toBeUndefined()
    expect(settings.protocolByRange.clauseByInertia).toBeUndefined()
  })
})

describe('resolveSettings — une clause adoptée change une valeur', () => {
  it('la version courante d’une clause remplace le défaut', () => {
    const settings = resolveFrom([['triage.smallGroupMax', 8]])
    expect(settings.triage.smallGroupMax).toBe(8)
    expect(settings.triage.collectiveMin).toBe(50) // les autres clés gardent leur défaut
  })

  it('une version seulement proposée ne change rien', () => {
    const clause = makeClause('triage.framingDays')
    const version = makeVersion(clause.id, 21, { status: 'proposed', adoptedAt: undefined })
    expect(resolveSettings([clause], [version]).triage.framingDays).toBe(14)
  })

  it('une version remplacée ne change rien — seule la courante compte', () => {
    const clause = makeClause('triage.consentMax')
    const superseded = makeVersion(clause.id, 3, { status: 'superseded' })
    const current = makeVersion(clause.id, 9, { adoptedAt: T1 })
    expect(resolveSettings([clause], [superseded, current]).triage.consentMax).toBe(9)
  })

  it('deux versions courantes (anomalie) — la plus récemment adoptée gagne', () => {
    const clause = makeClause('triage.objectionWindowHours')
    const older = makeVersion(clause.id, 24, { adoptedAt: T0 })
    const newer = makeVersion(clause.id, 96, { adoptedAt: T1 })
    expect(resolveSettings([clause], [older, newer]).triage.objectionWindowHours).toBe(96)
  })

  it('une clause archivée est ignorée', () => {
    const clause = makeClause('triage.smallGroupMax', { archivedAt: T1 })
    const version = makeVersion(clause.id, 12)
    expect(resolveSettings([clause], [version]).triage.smallGroupMax).toBe(5)
  })
})

describe('resolveSettings — requireEffects résolu', () => {
  it('« none » voté est appliqué', () => {
    expect(resolveFrom([['triage.requireEffects', 'none']]).triage.requireEffects).toBe('none')
  })

  it('« structural » voté est appliqué', () => {
    expect(resolveFrom([['triage.requireEffects', 'structural']]).triage.requireEffects).toBe(
      'structural',
    )
  })

  it('une valeur inconnue retombe sur « binding »', () => {
    expect(resolveFrom([['triage.requireEffects', 'everything']]).triage.requireEffects).toBe(
      'binding',
    )
  })
})

describe('resolveSettings — le protocole de consentement', () => {
  it('la clause protocols.consent résout l’identifiant', () => {
    const settings = resolveFrom([['protocols.consent', 'proto-consent-1']])
    expect(settings.protocolByRange.consent).toBe('proto-consent-1')
    expect(hasConsentProtocol(settings)).toBe(true)
  })

  it('consent introuvable (bundle corrompu) — objet complet, consent vide, jamais de crash', () => {
    expect(() => resolveSettings([], [])).not.toThrow()
    const settings = resolveSettings([], [])
    expect(settings.protocolByRange.consent).toBe('')
    expect(hasConsentProtocol(settings)).toBe(false)
    expect(settings.triage.smallGroupMax).toBe(5) // le reste de l’objet est utilisable
  })

  it('consent d’un mauvais type compte comme introuvable', () => {
    const settings = resolveFrom([['protocols.consent', 42]])
    expect(settings.protocolByRange.consent).toBe('')
    expect(hasConsentProtocol(settings)).toBe(false)
  })
})

describe('resolveSettings — les protocoles optionnels et l’héritage Ğ1', () => {
  it('nuanced, large, parametric et election se résolvent chacun', () => {
    const settings = resolveFrom([
      ['protocols.consent', 'p-consent'],
      ['protocols.nuanced', 'p-nuanced'],
      ['protocols.large', 'p-large'],
      ['protocols.parametric', 'p-parametric'],
      ['protocols.election', 'p-election'],
    ])
    expect(settings.protocolByRange.nuanced).toBe('p-nuanced')
    expect(settings.protocolByRange.large).toBe('p-large')
    expect(settings.protocolByRange.parametric).toBe('p-parametric')
    expect(settings.protocolByRange.election).toBe('p-election')
  })

  it('clauseByInertia complet (4 presets) est exposé', () => {
    const settings = resolveFrom([
      ['protocols.clauseByInertia.low', 'p-low'],
      ['protocols.clauseByInertia.standard', 'p-standard'],
      ['protocols.clauseByInertia.high', 'p-high'],
      ['protocols.clauseByInertia.max', 'p-max'],
    ])
    expect(settings.protocolByRange.clauseByInertia).toEqual({
      low: 'p-low',
      standard: 'p-standard',
      high: 'p-high',
      max: 'p-max',
    })
  })

  it('clauseByInertia partiel reste indéfini — l’appelant retombe sur consent', () => {
    const settings = resolveFrom([
      ['protocols.clauseByInertia.low', 'p-low'],
      ['protocols.clauseByInertia.standard', 'p-standard'],
      ['protocols.clauseByInertia.high', 'p-high'],
    ])
    expect(settings.protocolByRange.clauseByInertia).toBeUndefined()
  })
})

describe('resolveSettings — les valeurs invalides retombent sur les défauts, en silence', () => {
  it('un mauvais type sur une clé numérique est ignoré', () => {
    expect(resolveFrom([['triage.smallGroupMax', 'huit']]).triage.smallGroupMax).toBe(5)
  })

  it('un nombre négatif est ignoré', () => {
    expect(resolveFrom([['triage.framingDays', -3]]).triage.framingDays).toBe(14)
  })

  it('un ratio négatif est ignoré', () => {
    expect(resolveFrom([['triage.concernEscalateRatio', -0.5]]).triage.concernEscalateRatio).toBe(
      0.5,
    )
  })

  it('null est ignoré', () => {
    expect(resolveFrom([['triage.reviewDelayDays', null]]).triage.reviewDelayDays).toBe(90)
  })

  it('un identifiant de protocole vide compte comme non résolu', () => {
    const settings = resolveFrom([['protocols.nuanced', '   ']])
    expect(settings.protocolByRange.nuanced).toBeUndefined()
  })
})
