/**
 * Tests du moteur de périmètre — computeConcerned / expandCircleMembers.
 * Fonctions pures : fixtures assemblées à la main, aucun store.
 */
import { describe, expect, it } from 'vitest'
import { computeConcerned, expandCircleMembers } from '../../app/engine/impact'
import type { Circle, Decision, Mandate } from '../../app/types/domain'

const T0 = '2026-01-01T00:00:00Z'
const T9 = '2027-01-01T00:00:00Z'
const AUTHOR = 'p-author'

function mkCircle(over: Partial<Circle> = {}): Circle {
  return {
    id: 'c1',
    collectiveId: 'col',
    createdAt: T0,
    updatedAt: T0,
    name: 'Forgerons',
    purpose: '',
    memberIds: [],
    domains: [],
    ...over,
  }
}

function mkMandate(over: Partial<Mandate> = {}): Mandate {
  return {
    id: 'm1',
    collectiveId: 'col',
    createdAt: T0,
    updatedAt: T0,
    title: 'Modération',
    holderId: 'p-holder',
    originDecisionId: 'd0',
    domain: { circleIds: ['c1'], tags: [] },
    startsAt: T0,
    endsAt: T9,
    electorCircleId: 'c1',
    nominationMethod: 'consent',
    reports: [],
    status: 'active',
    ...over,
  }
}

function mkScope(over: Partial<Decision['scope']> = {}): Decision['scope'] {
  return { selfOnly: false, circleIds: [], personIds: [], ...over }
}

describe('computeConcerned — l\'union calculée, chaque entrée avec sa raison', () => {
  it('unit les trois sources : cercles, personnes nommées, titulaires de mandats', () => {
    const circles = [mkCircle({ id: 'c1', name: 'Forgerons', memberIds: ['p1', 'p2'] })]
    const mandates = [mkMandate({ holderId: 'p4', title: 'Modération' })]
    const scope = mkScope({ circleIds: ['c1'], personIds: ['p3'] })

    const result = computeConcerned(scope, circles, mandates, AUTHOR)

    expect(result).toHaveLength(4)
    expect(result).toContainEqual({ personId: 'p1', reason: 'membre du cercle Forgerons' })
    expect(result).toContainEqual({ personId: 'p2', reason: 'membre du cercle Forgerons' })
    expect(result).toContainEqual({ personId: 'p3', reason: 'nommé·e par l\'auteur' })
    expect(result).toContainEqual({ personId: 'p4', reason: 'titulaire du mandat Modération' })
  })

  it('dédoublonne — la première raison gagne, priorité cercle > nommé > mandat', () => {
    const circles = [mkCircle({ id: 'c1', name: 'Forgerons', memberIds: ['p1'] })]
    // p1 est membre du cercle ET nommé ET titulaire d'un mandat intersectant.
    const mandates = [mkMandate({ holderId: 'p1' })]
    const scope = mkScope({ circleIds: ['c1'], personIds: ['p1'] })

    const result = computeConcerned(scope, circles, mandates, AUTHOR)

    expect(result).toEqual([{ personId: 'p1', reason: 'membre du cercle Forgerons' }])
  })

  it('dédoublonne — nommé·e gagne sur titulaire de mandat', () => {
    const mandates = [mkMandate({ holderId: 'p1', domain: { circleIds: ['c1'], tags: [] } })]
    const scope = mkScope({ circleIds: ['c1'], personIds: ['p1'] })

    const result = computeConcerned(scope, [], mandates, AUTHOR)

    expect(result).toEqual([{ personId: 'p1', reason: 'nommé·e par l\'auteur' }])
  })

  it('exclut l\'auteur de toutes les sources', () => {
    const circles = [mkCircle({ id: 'c1', memberIds: [AUTHOR, 'p1'] })]
    const mandates = [mkMandate({ holderId: AUTHOR })]
    const scope = mkScope({ circleIds: ['c1'], personIds: [AUTHOR] })

    const result = computeConcerned(scope, circles, mandates, AUTHOR)

    expect(result.map(e => e.personId)).toEqual(['p1'])
  })

  it('n\'inclut un titulaire que si le domaine du mandat intersecte les cercles du périmètre', () => {
    const mandates = [
      mkMandate({ id: 'm1', holderId: 'p4', domain: { circleIds: ['c9'], tags: [] } }),
      mkMandate({ id: 'm2', holderId: 'p5', title: 'Trésorerie', domain: { circleIds: ['c2', 'c1'], tags: [] } }),
    ]
    const scope = mkScope({ circleIds: ['c1'] })

    const result = computeConcerned(scope, [], mandates, AUTHOR)

    expect(result).toEqual([{ personId: 'p5', reason: 'titulaire du mandat Trésorerie' }])
  })

  it('ignore les mandats non actifs', () => {
    const mandates = [mkMandate({ holderId: 'p4', status: 'expired' })]
    const scope = mkScope({ circleIds: ['c1'] })

    expect(computeConcerned(scope, [], mandates, AUTHOR)).toEqual([])
  })

  it('ignore sans erreur un cercle inconnu dans le périmètre', () => {
    const scope = mkScope({ circleIds: ['c-fantome'], personIds: ['p1'] })

    const result = computeConcerned(scope, [], [], AUTHOR)

    expect(result).toEqual([{ personId: 'p1', reason: 'nommé·e par l\'auteur' }])
  })
})

describe('expandCircleMembers — appartenance nominative, sans propagation', () => {
  it('réunit les membres directs de plusieurs cercles, dédoublonnés', () => {
    const circles = [
      mkCircle({ id: 'c1', memberIds: ['p1', 'p2'] }),
      mkCircle({ id: 'c2', name: 'Cuisine', memberIds: ['p2', 'p3'] }),
    ]

    const members = expandCircleMembers(['c1', 'c2'], circles)

    expect(members).toEqual(new Set(['p1', 'p2', 'p3']))
  })

  it('ne propage PAS l\'appartenance des cercles enfants au parent (emboîtement ≠ appartenance)', () => {
    // v2 : l'appartenance est nominative par cercle ; l'emboîtement
    // (parentCircleId) sert l'élargissement d'un cran, jamais le calcul.
    const parent = mkCircle({ id: 'c-parent', name: 'Village', memberIds: ['p1'] })
    const child = mkCircle({
      id: 'c-child',
      name: 'Hameau',
      memberIds: ['p2'],
      parentCircleId: 'c-parent',
    })

    expect(expandCircleMembers(['c-parent'], [parent, child])).toEqual(new Set(['p1']))
    // Ni dans l'autre sens : l'enfant n'hérite pas des membres du parent.
    expect(expandCircleMembers(['c-child'], [parent, child])).toEqual(new Set(['p2']))
    // L'élargissement est un geste explicite : ajouter le cercle au périmètre.
    expect(expandCircleMembers(['c-parent', 'c-child'], [parent, child]))
      .toEqual(new Set(['p1', 'p2']))
  })

  it('ignore un identifiant de cercle inconnu', () => {
    expect(expandCircleMembers(['c-fantome'], [mkCircle()])).toEqual(new Set())
  })
})
