import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import type { AppState, StateRepository } from '@domain/ports/StateRepository';
import { EventBus } from '@domain/events/EventBus';
import { createEnclos, addDragodinde } from '@domain/entities/Enclos';
import { createStartTimerHandler } from '@application/commands/StartTimer';
import { createStopTimerHandler } from '@application/commands/StopTimer';
import { createCreateEnclosHandler } from '@application/commands/CreateEnclos';
import { createDeleteEnclosHandler } from '@application/commands/DeleteEnclos';
import { createAddDragodindeHandler } from '@application/commands/AddDragodinde';
import { createRemoveDragodindeHandler } from '@application/commands/RemoveDragodinde';
import { createToggleGaugeHandler, createUpdateGaugeLevelHandler } from '@application/commands/UpdateGauge';
import { createRegisterAccouplementHandler } from '@application/commands/RegisterAccouplement';
import { createUpdateSettingsHandler } from '@application/commands/UpdateSettings';
import { createResetStatsHandler } from '@application/commands/ResetStats';
import { createRechargeGaugeHandler } from '@application/commands/RechargeGauge';
import { createResetTimerHandler, createClearEnclosHandler, createRenameEnclosHandler, createNouvelleFourneeHandler } from '@application/commands/EnclosActions';
import { createSaveWorkflowHandler } from '@application/commands/SaveWorkflow';
import { createCompleteTimerHandler } from '@application/commands/CompleteTimer';
import { createDeleteWorkflowHandler } from '@application/commands/DeleteWorkflow';
import { createRenameDragodindeHandler, createUpdateDdStatHandler, createUpdateDdSerenTargetHandler, createUpdateDdLevelTargetHandler, createReorderDragodindeHandler } from '@application/commands/DragodindeActions';
import { createImportWorkflowsHandler } from '@application/commands/ImportWorkflows';
import { createReorderEnclosHandler } from '@application/commands/ReorderEnclos';
import { createUpdateWorkflowHandler } from '@application/commands/UpdateWorkflow';
import type { WorkflowItem } from '@application/queries/GetWorkflows';

function makeState(): AppState {
  let enc = createEnclos(1);
  enc = addDragodinde(enc);
  enc.activeGauges = ['baffeur'];
  enc.gaugeLevels.baffeur = 50000;
  return {
    enclos: [enc], activeId: 1, nextEnclosId: 2,
    alarmSound: 'arpege', notifsEnabled: true, ntfyTopic: '',
    archivedStats: [], inventaire: {}, workflows: [], accouplements: [],
  };
}

function makeRepo(): StateRepository {
  return { load: vi.fn(async () => null), save: vi.fn() };
}

describe('StartTimer', () => {
  it('starts timer on enclos', () => {
    const state = makeState();
    const repo = makeRepo();
    const handler = createStartTimerHandler(state, repo);
    handler({ type: 'start-timer', enclosId: 1 });
    expect(state.enclos[0]!.timer.running).toBe(true);
    expect(state.enclos[0]!.timer.startTime).not.toBeNull();
    expect(state.enclos[0]!.timer.snapGauges).toHaveProperty('baffeur', 50000);
    expect(repo.save).toHaveBeenCalled();
  });

  it('ignores if already running', () => {
    const state = makeState();
    const repo = makeRepo();
    const handler = createStartTimerHandler(state, repo);
    handler({ type: 'start-timer', enclosId: 1 });
    handler({ type: 'start-timer', enclosId: 1 }); // second call
    expect(repo.save).toHaveBeenCalledTimes(1);
  });

  it('ignores if no gauges active', () => {
    const state = makeState();
    state.enclos[0]!.activeGauges = [];
    const repo = makeRepo();
    const handler = createStartTimerHandler(state, repo);
    handler({ type: 'start-timer', enclosId: 1 });
    expect(state.enclos[0]!.timer.running).toBe(false);
  });
});

describe('StopTimer', () => {
  it('stops running timer', () => {
    const state = makeState();
    const repo = makeRepo();
    createStartTimerHandler(state, repo)({ type: 'start-timer', enclosId: 1 });
    createStopTimerHandler(state, repo)({ type: 'stop-timer', enclosId: 1 });
    expect(state.enclos[0]!.timer.running).toBe(false);
    expect(state.enclos[0]!.timer.pausedAt).not.toBeNull();
  });
});

describe('CreateEnclos', () => {
  it('creates and activates new enclos', () => {
    const state = makeState();
    const repo = makeRepo();
    createCreateEnclosHandler(state, repo)({ type: 'create-enclos' });
    expect(state.enclos).toHaveLength(2);
    expect(state.activeId).toBe(2);
    expect(state.enclos[1]!.dragodindes).toHaveLength(1); // starts with 1 DD
  });
});

describe('DeleteEnclos', () => {
  it('deletes and switches active', () => {
    const state = makeState();
    const repo = makeRepo();
    const events = new EventBus();
    const handler = vi.fn();
    events.on('enclos-deleted', handler);
    createDeleteEnclosHandler(state, repo, events)({ type: 'delete-enclos', enclosId: 1 });
    expect(state.enclos).toHaveLength(0);
    expect(state.activeId).toBe('dashboard');
    expect(handler).toHaveBeenCalled();
  });
});

describe('AddDragodinde', () => {
  it('adds DD to enclos', () => {
    const state = makeState();
    const repo = makeRepo();
    createAddDragodindeHandler(state, repo)({ type: 'add-dragodinde', enclosId: 1 });
    expect(state.enclos[0]!.dragodindes).toHaveLength(2);
  });
});

describe('RemoveDragodinde', () => {
  it('removes DD from enclos', () => {
    const state = makeState();
    const repo = makeRepo();
    createRemoveDragodindeHandler(state, repo)({ type: 'remove-dragodinde', enclosId: 1, ddId: 1 });
    expect(state.enclos[0]!.dragodindes).toHaveLength(0);
  });
});

describe('ToggleGauge', () => {
  it('toggles gauge on', () => {
    const state = makeState();
    state.enclos[0]!.activeGauges = [];
    const repo = makeRepo();
    createToggleGaugeHandler(state, repo)({ type: 'toggle-gauge', enclosId: 1, gaugeId: 'foudroyeur' });
    expect(state.enclos[0]!.activeGauges).toContain('foudroyeur');
  });

  it('toggles gauge off', () => {
    const state = makeState();
    const repo = makeRepo();
    createToggleGaugeHandler(state, repo)({ type: 'toggle-gauge', enclosId: 1, gaugeId: 'baffeur' });
    expect(state.enclos[0]!.activeGauges).not.toContain('baffeur');
  });
});

describe('UpdateGaugeLevel', () => {
  it('clamps level between 0 and 100000', () => {
    const state = makeState();
    const repo = makeRepo();
    createUpdateGaugeLevelHandler(state, repo)({ type: 'update-gauge-level', enclosId: 1, gaugeId: 'baffeur', level: 150000 });
    expect(state.enclos[0]!.gaugeLevels.baffeur).toBe(100000);
  });
});

describe('RegisterAccouplement', () => {
  it('adds accouplement to state', () => {
    const state = makeState();
    const repo = makeRepo();
    const events = new EventBus();
    createRegisterAccouplementHandler(state, repo, events)({
      type: 'register-accouplement',
      parentA: 'Rousse', parentB: 'Dorée', baby: 'Dorée et Rousse', couples: 5, babiesObtained: 3,
    });
    expect(state.accouplements).toHaveLength(1);
    expect(state.accouplements[0]!.baby).toBe('Dorée et Rousse');
  });
});

describe('UpdateSettings', () => {
  it('updates alarm sound', () => {
    const state = makeState();
    const repo = makeRepo();
    createUpdateSettingsHandler(state, repo)({ type: 'update-settings', alarmSound: 'cloche' });
    expect(state.alarmSound).toBe('cloche');
  });

  it('persists inventaire', () => {
    const state = makeState();
    const repo = makeRepo();
    const inv = { Rousse: { m: 3, f: 2 }, Dorée: { m: 1, f: 4 } };
    createUpdateSettingsHandler(state, repo)({ type: 'update-settings', inventaire: inv });
    expect(state.inventaire).toEqual(inv);
    expect(repo.save).toHaveBeenCalled();
  });

  it('ne touche pas aux autres champs si seul inventaire est fourni', () => {
    const state = makeState();
    const repo = makeRepo();
    createUpdateSettingsHandler(state, repo)({ type: 'update-settings', inventaire: { Rousse: { m: 1, f: 0 } } });
    expect(state.alarmSound).toBe('arpege');
    expect(state.notifsEnabled).toBe(true);
  });
});

describe('SaveWorkflow', () => {
  it('crée un workflow avec matériaux et étapes', () => {
    const state = makeState();
    const repo = makeRepo();
    const handler = createSaveWorkflowHandler(state, repo);
    handler({
      type: 'save-workflow',
      target: 'Dorée et Rousse',
      qty: 4,
      materials: [{ race: 'Rousse', m: 4, f: 4 }, { race: 'Dorée', m: 4, f: 4 }],
      steps: [{ baby: 'Dorée et Rousse', parentA: 'Rousse', parentB: 'Dorée', couples: 4, gen: 2 }],
      repro: {},
    });
    expect(state.workflows).toHaveLength(1);
    expect(state.workflows[0]!.target).toBe('Dorée et Rousse');
    expect(state.workflows[0]!.qty).toBe(4);
    expect(state.workflows[0]!.materials).toHaveLength(2);
    expect(state.workflows[0]!.materials[0]!.needed).toBe(8); // m + f
    expect(state.workflows[0]!.steps).toHaveLength(1);
    expect(state.workflows[0]!.steps[0]!.gen).toBe(2);
    expect(state.workflows[0]!.steps[0]!.crossings).toHaveLength(1);
    expect(repo.save).toHaveBeenCalled();
  });

  it('groupe les étapes par génération', () => {
    const state = makeState();
    const repo = makeRepo();
    createSaveWorkflowHandler(state, repo)({
      type: 'save-workflow',
      target: 'Ebène',
      qty: 2,
      materials: [],
      steps: [
        { baby: 'Dorée et Rousse', parentA: 'Rousse', parentB: 'Dorée', couples: 4, gen: 2 },
        { baby: 'Amande et Dorée', parentA: 'Amande', parentB: 'Dorée', couples: 4, gen: 2 },
        { baby: 'Ebène', parentA: 'Amande et Dorée', parentB: 'Dorée et Rousse', couples: 2, gen: 3 },
      ],
      repro: {},
    });
    const wf = state.workflows[0]!;
    expect(wf.steps).toHaveLength(2); // gen 2 + gen 3
    expect(wf.steps[0]!.gen).toBe(2);
    expect(wf.steps[0]!.crossings).toHaveLength(2);
    expect(wf.steps[1]!.gen).toBe(3);
    expect(wf.steps[1]!.crossings).toHaveLength(1);
  });
});

describe('ResetStats', () => {
  it('clears stats and accouplements', () => {
    const state = makeState();
    state.archivedStats = [{ a: 1 }];
    state.accouplements = [{ parentA: 'A', parentB: 'B', baby: 'C', gen: 2, couples: 1, babiesObtained: 1, date: '' }];
    const repo = makeRepo();
    createResetStatsHandler(state, repo)({ type: 'reset-stats' });
    expect(state.archivedStats).toHaveLength(0);
    expect(state.accouplements).toHaveLength(0);
  });
});

describe('RechargeGauge', () => {
  function makeRunningState(): AppState {
    const state = makeState();
    // Simuler un timer démarré
    state.enclos[0]!.timer = {
      running: true,
      startTime: Date.now() - 3600_000, // démarré y'a 1h
      pausedAt: null,
      pausedMs: 0,
      snapGauges: { baffeur: 50000 },
      snapStats: {},
      gaugeRecharges: {},
    };
    return state;
  }

  it('enregistre une recharge avec le elapsed courant', () => {
    const state = makeRunningState();
    const repo = makeRepo();
    const handler = createRechargeGaugeHandler(state, repo);
    handler({ type: 'recharge-gauge', enclosId: 1, gaugeId: 'baffeur', level: 90000 });

    const recharges = state.enclos[0]!.timer.gaugeRecharges['baffeur'];
    expect(recharges).toHaveLength(1);
    expect(recharges![0]!.level).toBe(90000);
    // atSec ≈ 3600 sec (1h de timer)
    expect(recharges![0]!.atSec).toBeGreaterThan(3500);
    expect(recharges![0]!.atSec).toBeLessThan(3700);
  });

  it('met à jour gaugeLevels pour l\'affichage', () => {
    const state = makeRunningState();
    const repo = makeRepo();
    createRechargeGaugeHandler(state, repo)({ type: 'recharge-gauge', enclosId: 1, gaugeId: 'baffeur', level: 75000 });
    expect(state.enclos[0]!.timer.gaugeRecharges['baffeur']![0]!.level).toBe(75000);
    expect(repo.save).toHaveBeenCalled();
  });

  it('recharges proches (<2s) sont consolidées (saisie temps réel)', () => {
    const state = makeRunningState();
    const repo = makeRepo();
    const handler = createRechargeGaugeHandler(state, repo);
    handler({ type: 'recharge-gauge', enclosId: 1, gaugeId: 'baffeur', level: 70000 });
    handler({ type: 'recharge-gauge', enclosId: 1, gaugeId: 'baffeur', level: 90000 });
    // Les deux appels sont quasi-simultanés → consolidés en une seule entrée
    const arr = state.enclos[0]!.timer.gaugeRecharges['baffeur']!;
    expect(arr).toHaveLength(1);
    expect(arr[0]!.level).toBe(90000);
  });

  it('ignore si le timer n\'est pas en cours', () => {
    const state = makeState();
    // timer non démarré
    const repo = makeRepo();
    createRechargeGaugeHandler(state, repo)({ type: 'recharge-gauge', enclosId: 1, gaugeId: 'baffeur', level: 90000 });
    expect(state.enclos[0]!.timer.gaugeRecharges['baffeur']).toBeUndefined();
    expect(repo.save).not.toHaveBeenCalled();
  });

  it('reset-timer efface les recharges', () => {
    const state = makeRunningState();
    const repo = makeRepo();
    createRechargeGaugeHandler(state, repo)({ type: 'recharge-gauge', enclosId: 1, gaugeId: 'baffeur', level: 90000 });
    expect(state.enclos[0]!.timer.gaugeRecharges['baffeur']).toHaveLength(1);
    createResetTimerHandler(state, repo)({ type: 'reset-timer', enclosId: 1 });
    expect(state.enclos[0]!.timer.gaugeRecharges).toEqual({});
  });
});

describe('CompleteTimer', () => {
  afterEach(() => {
    vi.useRealTimers();
  });

  function makeRunningTimerState(): AppState {
    const state = makeState();
    const enc = state.enclos[0]!;
    enc.activeGauges = ['foudroyeur'];
    enc.gaugeLevels.foudroyeur = 80000;
    enc.dragodindes[0]!.stats.endurance = 100;
    enc.timer = {
      running: true,
      startTime: Date.now() - 7200_000, // démarré y'a 2h
      pausedAt: null,
      pausedMs: 0,
      snapGauges: { foudroyeur: 80000 },
      snapStats: { 1: { endurance: 100 } },
      gaugeRecharges: {},
    };
    enc.alerted = {};
    return state;
  }

  it('arrête le timer et persiste les stats finales', () => {
    vi.useFakeTimers();
    const now = Date.now();
    vi.setSystemTime(now);

    const state = makeState();
    const enc = state.enclos[0]!;
    enc.activeGauges = ['foudroyeur'];
    enc.gaugeLevels.foudroyeur = 80000;
    enc.dragodindes[0]!.stats.endurance = 100;
    enc.timer = {
      running: true,
      startTime: now - 7200_000,
      pausedAt: null,
      pausedMs: 0,
      snapGauges: { foudroyeur: 80000 },
      snapStats: { 1: { endurance: 100 } },
      gaugeRecharges: {},
    };
    enc.alerted = {};

    const repo = makeRepo();
    const events = new EventBus();
    const handler = createCompleteTimerHandler(state, repo, events);
    handler({ type: 'complete-timer', enclosId: 1 });

    expect(enc.timer.running).toBe(false);
    expect(enc.timer.pausedAt).toBe(now);
    expect(enc.alerted['__done__']).toBe(true);
    // endurance doit avoir augmenté par rapport à la valeur initiale
    expect(enc.dragodindes[0]!.stats.endurance).toBeGreaterThan(100);
    expect(repo.save).toHaveBeenCalled();
  });

  it('émet l\'événement timer-completed', () => {
    vi.useFakeTimers();
    vi.setSystemTime(Date.now());

    const state = makeRunningTimerState();
    const repo = makeRepo();
    const events = new EventBus();
    const eventHandler = vi.fn();
    events.on('timer-completed', eventHandler);

    createCompleteTimerHandler(state, repo, events)({ type: 'complete-timer', enclosId: 1 });
    expect(eventHandler).toHaveBeenCalledWith(expect.objectContaining({ type: 'timer-completed' }));
  });

  it('ignore si l\'enclos n\'existe pas', () => {
    const state = makeState();
    const repo = makeRepo();
    const events = new EventBus();
    createCompleteTimerHandler(state, repo, events)({ type: 'complete-timer', enclosId: 999 });
    expect(repo.save).not.toHaveBeenCalled();
  });

  it('ignore si le timer n\'est pas en cours', () => {
    const state = makeState();
    const repo = makeRepo();
    const events = new EventBus();
    createCompleteTimerHandler(state, repo, events)({ type: 'complete-timer', enclosId: 1 });
    expect(repo.save).not.toHaveBeenCalled();
  });

  it('ignore si déjà complété (__done__)', () => {
    const state = makeRunningTimerState();
    state.enclos[0]!.alerted['__done__'] = true;
    const repo = makeRepo();
    const events = new EventBus();
    createCompleteTimerHandler(state, repo, events)({ type: 'complete-timer', enclosId: 1 });
    expect(repo.save).not.toHaveBeenCalled();
  });
});

describe('DeleteWorkflow', () => {
  it('supprime un workflow par id', () => {
    const state = makeState();
    state.workflows = [{ id: 42, name: 'wf1', target: 'Dorée', qty: 1, createdAt: 0, materials: [], steps: [] }] as WorkflowItem[];
    const repo = makeRepo();
    createDeleteWorkflowHandler(state, repo)({ type: 'delete-workflow', workflowId: 42 });
    expect(state.workflows).toHaveLength(0);
    expect(repo.save).toHaveBeenCalled();
  });

  it('ne fait rien si le workflow n\'existe pas', () => {
    const state = makeState();
    state.workflows = [{ id: 42, name: 'wf1', target: 'Dorée', qty: 1, createdAt: 0, materials: [], steps: [] }] as WorkflowItem[];
    const repo = makeRepo();
    createDeleteWorkflowHandler(state, repo)({ type: 'delete-workflow', workflowId: 999 });
    expect(state.workflows).toHaveLength(1);
    expect(repo.save).toHaveBeenCalled();
  });
});

describe('RenameDragodinde', () => {
  it('renomme une dragodinde', () => {
    const state = makeState();
    const repo = makeRepo();
    createRenameDragodindeHandler(state, repo)({ type: 'rename-dragodinde', enclosId: 1, ddId: 1, name: 'Flamme' });
    expect(state.enclos[0]!.dragodindes[0]!.name).toBe('Flamme');
    expect(repo.save).toHaveBeenCalled();
  });

  it('trim le nom', () => {
    const state = makeState();
    const repo = makeRepo();
    createRenameDragodindeHandler(state, repo)({ type: 'rename-dragodinde', enclosId: 1, ddId: 1, name: '  Flamme  ' });
    expect(state.enclos[0]!.dragodindes[0]!.name).toBe('Flamme');
  });

  it('ignore si le nom est vide ou whitespace', () => {
    const state = makeState();
    const repo = makeRepo();
    createRenameDragodindeHandler(state, repo)({ type: 'rename-dragodinde', enclosId: 1, ddId: 1, name: '   ' });
    expect(state.enclos[0]!.dragodindes[0]!.name).toBe('Dragodinde 1');
    expect(repo.save).not.toHaveBeenCalled();
  });

  it('ignore si l\'enclos n\'existe pas', () => {
    const state = makeState();
    const repo = makeRepo();
    createRenameDragodindeHandler(state, repo)({ type: 'rename-dragodinde', enclosId: 999, ddId: 1, name: 'X' });
    expect(repo.save).not.toHaveBeenCalled();
  });

  it('ignore si la DD n\'existe pas', () => {
    const state = makeState();
    const repo = makeRepo();
    createRenameDragodindeHandler(state, repo)({ type: 'rename-dragodinde', enclosId: 1, ddId: 999, name: 'X' });
    expect(repo.save).not.toHaveBeenCalled();
  });
});

describe('UpdateDdStat', () => {
  it('met à jour une stat de la DD', () => {
    const state = makeState();
    const repo = makeRepo();
    createUpdateDdStatHandler(state, repo)({ type: 'update-dd-stat', enclosId: 1, ddId: 1, stat: 'endurance', value: 5000 });
    expect(state.enclos[0]!.dragodindes[0]!.stats.endurance).toBe(5000);
    expect(repo.save).toHaveBeenCalled();
  });

  it('ignore si l\'enclos n\'existe pas', () => {
    const state = makeState();
    const repo = makeRepo();
    createUpdateDdStatHandler(state, repo)({ type: 'update-dd-stat', enclosId: 999, ddId: 1, stat: 'endurance', value: 5000 });
    expect(repo.save).not.toHaveBeenCalled();
  });

  it('ignore si la DD n\'existe pas', () => {
    const state = makeState();
    const repo = makeRepo();
    createUpdateDdStatHandler(state, repo)({ type: 'update-dd-stat', enclosId: 1, ddId: 999, stat: 'endurance', value: 5000 });
    expect(repo.save).not.toHaveBeenCalled();
  });
});

describe('UpdateDdSerenTarget', () => {
  it('définit une cible de sérénité', () => {
    const state = makeState();
    const repo = makeRepo();
    createUpdateDdSerenTargetHandler(state, repo)({ type: 'update-dd-seren-target', enclosId: 1, ddId: 1, target: -2000 });
    expect(state.enclos[0]!.dragodindes[0]!.sereniteTarget).toBe(-2000);
    expect(repo.save).toHaveBeenCalled();
  });

  it('accepte null pour retirer la cible', () => {
    const state = makeState();
    state.enclos[0]!.dragodindes[0]!.sereniteTarget = -2000;
    const repo = makeRepo();
    createUpdateDdSerenTargetHandler(state, repo)({ type: 'update-dd-seren-target', enclosId: 1, ddId: 1, target: null });
    expect(state.enclos[0]!.dragodindes[0]!.sereniteTarget).toBeNull();
  });

  it('ignore si l\'enclos n\'existe pas', () => {
    const state = makeState();
    const repo = makeRepo();
    createUpdateDdSerenTargetHandler(state, repo)({ type: 'update-dd-seren-target', enclosId: 999, ddId: 1, target: -2000 });
    expect(repo.save).not.toHaveBeenCalled();
  });

  it('ignore si la DD n\'existe pas', () => {
    const state = makeState();
    const repo = makeRepo();
    createUpdateDdSerenTargetHandler(state, repo)({ type: 'update-dd-seren-target', enclosId: 1, ddId: 999, target: -2000 });
    expect(repo.save).not.toHaveBeenCalled();
  });
});

describe('UpdateDdLevelTarget', () => {
  it('définit une cible de niveau', () => {
    const state = makeState();
    const repo = makeRepo();
    createUpdateDdLevelTargetHandler(state, repo)({ type: 'update-dd-level-target', enclosId: 1, ddId: 1, target: 100 });
    expect(state.enclos[0]!.dragodindes[0]!.levelTarget).toBe(100);
    expect(repo.save).toHaveBeenCalled();
  });

  it('accepte null pour retirer la cible', () => {
    const state = makeState();
    state.enclos[0]!.dragodindes[0]!.levelTarget = 100;
    const repo = makeRepo();
    createUpdateDdLevelTargetHandler(state, repo)({ type: 'update-dd-level-target', enclosId: 1, ddId: 1, target: null });
    expect(state.enclos[0]!.dragodindes[0]!.levelTarget).toBeNull();
  });

  it('ignore si l\'enclos n\'existe pas', () => {
    const state = makeState();
    const repo = makeRepo();
    createUpdateDdLevelTargetHandler(state, repo)({ type: 'update-dd-level-target', enclosId: 999, ddId: 1, target: 100 });
    expect(repo.save).not.toHaveBeenCalled();
  });
});

describe('ReorderDragodinde', () => {
  it('réordonne les DDs dans l\'enclos', () => {
    const state = makeState();
    // Ajouter une 2e DD
    state.enclos[0] = addDragodinde(state.enclos[0]!);
    const repo = makeRepo();
    const dd1Id = state.enclos[0]!.dragodindes[0]!.id;
    const dd2Id = state.enclos[0]!.dragodindes[1]!.id;
    createReorderDragodindeHandler(state, repo)({ type: 'reorder-dragodinde', enclosId: 1, fromDdId: dd1Id, toDdId: dd2Id });
    expect(state.enclos[0]!.dragodindes[0]!.id).toBe(dd2Id);
    expect(state.enclos[0]!.dragodindes[1]!.id).toBe(dd1Id);
    expect(repo.save).toHaveBeenCalled();
  });

  it('ignore si l\'enclos n\'existe pas', () => {
    const state = makeState();
    const repo = makeRepo();
    createReorderDragodindeHandler(state, repo)({ type: 'reorder-dragodinde', enclosId: 999, fromDdId: 1, toDdId: 2 });
    expect(repo.save).not.toHaveBeenCalled();
  });

  it('ignore si un des ddId n\'existe pas', () => {
    const state = makeState();
    const repo = makeRepo();
    createReorderDragodindeHandler(state, repo)({ type: 'reorder-dragodinde', enclosId: 1, fromDdId: 1, toDdId: 999 });
    expect(repo.save).not.toHaveBeenCalled();
  });
});

describe('ImportWorkflows', () => {
  it('importe des workflows dans la liste existante', () => {
    const state = makeState();
    const repo = makeRepo();
    const wf: WorkflowItem = { id: 10, name: 'Import1', target: 'Dorée', qty: 2, createdAt: 0, materials: [], steps: [] };
    createImportWorkflowsHandler(state, repo)({ type: 'import-workflows', workflows: [wf] });
    expect(state.workflows).toHaveLength(1);
    expect((state.workflows[0] as WorkflowItem).target).toBe('Dorée');
    expect(repo.save).toHaveBeenCalled();
  });

  it('réattribue un id si doublon', () => {
    const state = makeState();
    const existingWf: WorkflowItem = { id: 10, name: 'Existant', target: 'Rousse', qty: 1, createdAt: 0, materials: [], steps: [] };
    state.workflows = [existingWf] as any;
    const repo = makeRepo();
    const importWf: WorkflowItem = { id: 10, name: 'Import1', target: 'Dorée', qty: 2, createdAt: 0, materials: [], steps: [] };
    createImportWorkflowsHandler(state, repo)({ type: 'import-workflows', workflows: [importWf] });
    expect(state.workflows).toHaveLength(2);
    // L'id du workflow importé doit avoir changé (plus 10)
    expect((state.workflows[1] as WorkflowItem).id).not.toBe(10);
    expect((state.workflows[1] as WorkflowItem).target).toBe('Dorée');
  });

  it('importe plusieurs workflows sans conflit d\'id', () => {
    const state = makeState();
    const repo = makeRepo();
    const wf1: WorkflowItem = { id: 1, name: 'A', target: 'A', qty: 1, createdAt: 0, materials: [], steps: [] };
    const wf2: WorkflowItem = { id: 2, name: 'B', target: 'B', qty: 1, createdAt: 0, materials: [], steps: [] };
    createImportWorkflowsHandler(state, repo)({ type: 'import-workflows', workflows: [wf1, wf2] });
    expect(state.workflows).toHaveLength(2);
  });
});

describe('ReorderEnclos', () => {
  it('réordonne les enclos', () => {
    const state = makeState();
    // Ajouter un 2e enclos
    const enc2 = createEnclos(2, 'Enclos 2');
    state.enclos.push(enc2);
    const repo = makeRepo();
    createReorderEnclosHandler(state, repo)({ type: 'reorder-enclos', fromIndex: 0, toIndex: 1 });
    expect(state.enclos[0]!.id).toBe(2);
    expect(state.enclos[1]!.id).toBe(1);
    expect(repo.save).toHaveBeenCalled();
  });

  it('ignore si fromIndex est négatif', () => {
    const state = makeState();
    const repo = makeRepo();
    createReorderEnclosHandler(state, repo)({ type: 'reorder-enclos', fromIndex: -1, toIndex: 0 });
    expect(repo.save).not.toHaveBeenCalled();
  });

  it('ignore si toIndex est négatif', () => {
    const state = makeState();
    const repo = makeRepo();
    createReorderEnclosHandler(state, repo)({ type: 'reorder-enclos', fromIndex: 0, toIndex: -1 });
    expect(repo.save).not.toHaveBeenCalled();
  });

  it('ignore si fromIndex dépasse la taille du tableau', () => {
    const state = makeState();
    const repo = makeRepo();
    createReorderEnclosHandler(state, repo)({ type: 'reorder-enclos', fromIndex: 5, toIndex: 0 });
    expect(repo.save).not.toHaveBeenCalled();
  });

  it('ignore si toIndex dépasse la taille du tableau', () => {
    const state = makeState();
    const repo = makeRepo();
    createReorderEnclosHandler(state, repo)({ type: 'reorder-enclos', fromIndex: 0, toIndex: 5 });
    expect(repo.save).not.toHaveBeenCalled();
  });
});

describe('UpdateWorkflow', () => {
  function makeWorkflowState(): AppState {
    const state = makeState();
    const wf: WorkflowItem = {
      id: 1, name: 'Workflow 1', target: 'Dorée', qty: 2, createdAt: 0,
      materials: [{ name: 'Rousse', needed: 4, done: 0 }, { name: 'Dorée', needed: 4, done: 0 }],
      steps: [{
        gen: 2,
        crossings: [{ race: 'Dorée et Rousse', needed: 2, parentA: 'Rousse', parentB: 'Dorée', couples: 2, repro: 0, done: 0 }],
      }],
    };
    state.workflows = [wf] as any;
    return state;
  }

  it('met à jour le done d\'un matériau', () => {
    const state = makeWorkflowState();
    const repo = makeRepo();
    createUpdateWorkflowHandler(state, repo)({ type: 'update-workflow', workflowId: 1, materialIdx: 0, done: 3 });
    expect((state.workflows[0] as WorkflowItem).materials[0]!.done).toBe(3);
    expect(repo.save).toHaveBeenCalled();
  });

  it('met à jour le done d\'un crossing', () => {
    const state = makeWorkflowState();
    const repo = makeRepo();
    createUpdateWorkflowHandler(state, repo)({ type: 'update-workflow', workflowId: 1, stepIdx: 0, crossingIdx: 0, done: 1 });
    expect((state.workflows[0] as WorkflowItem).steps[0]!.crossings[0]!.done).toBe(1);
    expect(repo.save).toHaveBeenCalled();
  });

  it('clamp done à 0 minimum pour matériau', () => {
    const state = makeWorkflowState();
    const repo = makeRepo();
    createUpdateWorkflowHandler(state, repo)({ type: 'update-workflow', workflowId: 1, materialIdx: 0, done: -5 });
    expect((state.workflows[0] as WorkflowItem).materials[0]!.done).toBe(0);
  });

  it('clamp done à 0 minimum pour crossing', () => {
    const state = makeWorkflowState();
    const repo = makeRepo();
    createUpdateWorkflowHandler(state, repo)({ type: 'update-workflow', workflowId: 1, stepIdx: 0, crossingIdx: 0, done: -3 });
    expect((state.workflows[0] as WorkflowItem).steps[0]!.crossings[0]!.done).toBe(0);
  });

  it('ignore si le workflow n\'existe pas', () => {
    const state = makeWorkflowState();
    const repo = makeRepo();
    createUpdateWorkflowHandler(state, repo)({ type: 'update-workflow', workflowId: 999, materialIdx: 0, done: 3 });
    // L'état ne change pas mais save est quand même appelé (pas de guard sur save)
    expect((state.workflows[0] as WorkflowItem).materials[0]!.done).toBe(0);
  });

  it('ignore si materialIdx hors limites', () => {
    const state = makeWorkflowState();
    const repo = makeRepo();
    createUpdateWorkflowHandler(state, repo)({ type: 'update-workflow', workflowId: 1, materialIdx: 99, done: 3 });
    expect((state.workflows[0] as WorkflowItem).materials[0]!.done).toBe(0);
  });

  it('ignore si stepIdx ou crossingIdx hors limites', () => {
    const state = makeWorkflowState();
    const repo = makeRepo();
    createUpdateWorkflowHandler(state, repo)({ type: 'update-workflow', workflowId: 1, stepIdx: 99, crossingIdx: 0, done: 1 });
    expect((state.workflows[0] as WorkflowItem).steps[0]!.crossings[0]!.done).toBe(0);
  });
});

describe('ClearEnclos', () => {
  it('vide les DDs, jauges et timer de l\'enclos', () => {
    const state = makeState();
    const repo = makeRepo();
    createClearEnclosHandler(state, repo)({ type: 'clear-enclos', enclosId: 1 });
    const enc = state.enclos[0]!;
    expect(enc.dragodindes).toHaveLength(0);
    expect(enc.nextDdId).toBe(1);
    expect(enc.activeGauges).toHaveLength(0);
    expect(enc.gaugeLevels.baffeur).toBe(0);
    expect(enc.timer.running).toBe(false);
    expect(enc.timer.snapGauges).toEqual({});
    expect(enc.alerted).toEqual({});
    expect(repo.save).toHaveBeenCalled();
  });

  it('ignore si l\'enclos n\'existe pas', () => {
    const state = makeState();
    const repo = makeRepo();
    createClearEnclosHandler(state, repo)({ type: 'clear-enclos', enclosId: 999 });
    expect(repo.save).not.toHaveBeenCalled();
  });
});

describe('RenameEnclos', () => {
  it('renomme un enclos', () => {
    const state = makeState();
    const repo = makeRepo();
    createRenameEnclosHandler(state, repo)({ type: 'rename-enclos', enclosId: 1, name: 'Mon enclos' });
    expect(state.enclos[0]!.name).toBe('Mon enclos');
    expect(repo.save).toHaveBeenCalled();
  });

  it('trim le nom', () => {
    const state = makeState();
    const repo = makeRepo();
    createRenameEnclosHandler(state, repo)({ type: 'rename-enclos', enclosId: 1, name: '  Paddock  ' });
    expect(state.enclos[0]!.name).toBe('Paddock');
  });

  it('ignore si le nom est vide ou whitespace', () => {
    const state = makeState();
    const repo = makeRepo();
    createRenameEnclosHandler(state, repo)({ type: 'rename-enclos', enclosId: 1, name: '   ' });
    expect(state.enclos[0]!.name).toBe('Enclos 1');
    expect(repo.save).not.toHaveBeenCalled();
  });

  it('ignore si l\'enclos n\'existe pas', () => {
    const state = makeState();
    const repo = makeRepo();
    createRenameEnclosHandler(state, repo)({ type: 'rename-enclos', enclosId: 999, name: 'Test' });
    expect(repo.save).not.toHaveBeenCalled();
  });
});

describe('NouvelleFournee', () => {
  it('réinitialise l\'enclos avec une DD fraîche et jauges à 0', () => {
    const state = makeState();
    const enc = state.enclos[0]!;
    // Simuler un état "usé"
    enc.gaugeLevels.baffeur = 80000;
    enc.alerted['foudroyeur'] = true;
    enc.timer = {
      running: true, startTime: Date.now() - 1000, pausedAt: null, pausedMs: 0,
      snapGauges: { baffeur: 80000 }, snapStats: {}, gaugeRecharges: {},
    };
    const repo = makeRepo();
    createNouvelleFourneeHandler(state, repo)({ type: 'nouvelle-fournee', enclosId: 1 });

    expect(enc.timer.running).toBe(false);
    expect(enc.timer.startTime).toBeNull();
    expect(enc.timer.gaugeRecharges).toEqual({});
    expect(enc.alerted).toEqual({});
    expect(enc.gaugeLevels.baffeur).toBe(0);
    expect(enc.gaugeLevels.foudroyeur).toBe(0);
    expect(enc.dragodindes).toHaveLength(1);
    expect(enc.dragodindes[0]!.id).toBe(1);
    expect(enc.dragodindes[0]!.stats.endurance).toBe(0);
    expect(enc.nextDdId).toBe(2);
    expect(repo.save).toHaveBeenCalled();
  });

  it('ignore si l\'enclos n\'existe pas', () => {
    const state = makeState();
    const repo = makeRepo();
    createNouvelleFourneeHandler(state, repo)({ type: 'nouvelle-fournee', enclosId: 999 });
    expect(repo.save).not.toHaveBeenCalled();
  });
});
