import React from "react";
import { render, fireEvent, act, waitFor } from "@testing-library/react-native";

const mockNav = { goBack: jest.fn(), navigate: jest.fn() };
const mockRoute: {
  params: {
    interventionId: number;
    brandColor: string;
    machineId: number;
    machineHasPhoto?: boolean;
  };
} = {
  params: { interventionId: 5, brandColor: "#1E56A8", machineId: 10, machineHasPhoto: false },
};

jest.mock("@react-navigation/native", () => ({
  useRoute: () => mockRoute,
  useNavigation: () => mockNav,
}));

// --- Expo Camera: capture la ref pour tester takePictureAsync ---
const mockCam = {
  takePicture: jest.fn(),
  granted: true,
  requestResult: { granted: true } as { granted: boolean },
};
jest.mock("expo-camera", () => {
  const ReactLocal = require("react");
  const { View } = require("react-native");
  const CameraView = ReactLocal.forwardRef(function CameraView(
    props: unknown,
    ref: React.Ref<{ takePictureAsync: jest.Mock }>
  ) {
    ReactLocal.useImperativeHandle(ref, () => ({
      takePictureAsync: mockCam.takePicture,
    }));
    return ReactLocal.createElement(View, props);
  });
  return {
    CameraView,
    useCameraPermissions: () => [
      { granted: mockCam.granted },
      jest.fn().mockResolvedValue(mockCam.requestResult),
    ],
  };
});

// --- Services mocks ---
jest.mock("../../../src/core/stepRepository", () => ({
  loadStepsLocally: jest.fn(),
  completeStepLocally: jest.fn(),
  validateStepValue: jest.fn(),
}));
jest.mock("../../../src/core/localDatabase", () => ({
  clearWorkflowDraft: jest.fn(),
  enqueueSyncAction: jest.fn(),
  loadWorkflowDraft: jest.fn(),
  saveWorkflowDraft: jest.fn(),
}));
jest.mock("../../../src/services/syncService", () => ({
  isNetworkOnline: jest.fn(),
}));
jest.mock("../../../src/services/interventionsApi", () => ({
  completeMobileWorkflow: jest.fn(),
  fetchAndStoreInterventionSteps: jest.fn(),
  saveStepToServer: jest.fn(),
  uploadMachinePhoto: jest.fn(),
}));

// --- Sub-components : mocks qui exposent les callbacks via state ---
const mockSubComps = {
  signatureCardProps: null as null | {
    onConfirm?: (sigs: unknown) => void;
    onCancel?: () => void;
    onDraftChange?: (d: Record<string, unknown>) => void;
    errorMessage?: string;
  },
  cameraCaptureProps: null as null | { onCaptured?: (uri: string) => void },
  signatureCaptureProps: null as null | { onCaptured?: (uri: string) => void },
};
jest.mock("../../../src/features/missions/SignatureFinishCard", () => {
  const { View, Text, Pressable } = require("react-native");
  return (props: {
    onConfirm?: (sigs: unknown) => void;
    onCancel?: () => void;
    onDraftChange?: (d: Record<string, unknown>) => void;
    errorMessage?: string;
  }) => {
    mockSubComps.signatureCardProps = props;
    const ReactLocal = require("react");
    return ReactLocal.createElement(
      View,
      { testID: "sig-card" },
      ReactLocal.createElement(Text, null, "SignatureFinishCard"),
      ReactLocal.createElement(
        Pressable,
        {
          testID: "sig-fire-confirm",
          onPress: () =>
            props.onConfirm?.({
              signature_technicien: "T",
              signature_client: "C",
              client_present: true,
            }),
        },
        ReactLocal.createElement(Text, null, "fire-confirm")
      ),
      ReactLocal.createElement(
        Pressable,
        {
          testID: "sig-fire-cancel",
          onPress: () => props.onCancel?.(),
        },
        ReactLocal.createElement(Text, null, "fire-cancel")
      ),
      ReactLocal.createElement(
        Pressable,
        {
          testID: "sig-fire-draft",
          onPress: () =>
            props.onDraftChange?.({
              techData: "td",
              clientData: "cd",
              clientPresent: false,
            }),
        },
        ReactLocal.createElement(Text, null, "fire-draft")
      )
    );
  };
});
jest.mock("../../../src/shared/CameraCapture", () => {
  const { View, Text, Pressable } = require("react-native");
  return (props: { onCaptured?: (uri: string) => void }) => {
    mockSubComps.cameraCaptureProps = props;
    const ReactLocal = require("react");
    return ReactLocal.createElement(
      View,
      { testID: "cam-capture" },
      ReactLocal.createElement(
        Pressable,
        {
          testID: "cam-fire",
          onPress: () =>
            props.onCaptured?.("file:///photo.jpg"),
        },
        ReactLocal.createElement(Text, null, "fire-photo")
      )
    );
  };
});
jest.mock("../../../src/shared/SignatureCapture", () => {
  const { View, Text, Pressable } = require("react-native");
  return (props: { onCaptured?: (uri: string) => void }) => {
    mockSubComps.signatureCaptureProps = props;
    const ReactLocal = require("react");
    return ReactLocal.createElement(
      View,
      { testID: "sig-capture" },
      ReactLocal.createElement(
        Pressable,
        {
          testID: "sig-capture-fire",
          onPress: () =>
            props.onCaptured?.("file:///sig.png"),
        },
        ReactLocal.createElement(Text, null, "fire-sig")
      )
    );
  };
});

jest.mock("../../../src/services/devisApi", () => ({
  getDevisForMachine: jest.fn(),
  openDevisPdf: jest.fn(),
}));

import * as devisApi from "../../../src/services/devisApi";
import * as stepRepository from "../../../src/core/stepRepository";
import * as localDatabase from "../../../src/core/localDatabase";
import * as syncService from "../../../src/services/syncService";
import * as interventionsApi from "../../../src/services/interventionsApi";
import WorkflowInterventionScreen from "../../../src/features/missions/WorkflowInterventionScreen";

const loadSteps = stepRepository.loadStepsLocally as jest.Mock;
const completeStep = stepRepository.completeStepLocally as jest.Mock;
const validateStep = stepRepository.validateStepValue as jest.Mock;
const loadDraft = localDatabase.loadWorkflowDraft as jest.Mock;
const saveDraft = localDatabase.saveWorkflowDraft as jest.Mock;
const clearDraft = localDatabase.clearWorkflowDraft as jest.Mock;
const enqueue = localDatabase.enqueueSyncAction as jest.Mock;
const isOnline = syncService.isNetworkOnline as jest.Mock;
const fetchSteps = interventionsApi.fetchAndStoreInterventionSteps as jest.Mock;
const saveStep = interventionsApi.saveStepToServer as jest.Mock;
const completeWorkflow = interventionsApi.completeMobileWorkflow as jest.Mock;
const uploadMachinePhoto = interventionsApi.uploadMachinePhoto as jest.Mock;
const getDevis = devisApi.getDevisForMachine as jest.Mock;
const openDevis = devisApi.openDevisPdf as jest.Mock;

type Step = {
  id: string;
  intervention_id: number;
  order_index: number;
  type: string;
  label: string;
  required: boolean;
  status: string;
  value: unknown;
  version: number;
  updated_at: string;
  metadata?: Record<string, unknown>;
  options?: string[];
  unit?: string | null;
};

const step = (over: Partial<Step> = {}): Step => ({
  id: "s1",
  intervention_id: 5,
  order_index: 0,
  type: "text",
  label: "Etape",
  required: false,
  status: "todo",
  value: null,
  version: 1,
  updated_at: "x",
  metadata: { phase: "P1" },
  ...over,
});

describe("WorkflowInterventionScreen", () => {
  beforeEach(() => {
    jest.clearAllMocks();
    mockRoute.params = { interventionId: 5, brandColor: "#1E56A8", machineId: 10, machineHasPhoto: false };
    mockCam.granted = true;
    mockCam.requestResult = { granted: true };
    mockCam.takePicture.mockReset();
    mockSubComps.signatureCardProps = null;
    mockSubComps.cameraCaptureProps = null;
    mockSubComps.signatureCaptureProps = null;
    fetchSteps.mockResolvedValue({ count: 0, workflowVersion: 2, workflowStatus: "in_progress" });
    loadSteps.mockResolvedValue([]);
    loadDraft.mockResolvedValue(null);
    validateStep.mockReturnValue(null);
    isOnline.mockResolvedValue(true);
    completeStep.mockImplementation(async (s: Step, value: unknown) => ({
      ...s,
      value,
      status: "done",
      version: s.version + 1,
    }));
    saveStep.mockResolvedValue({ version: 3, status: "in_progress" });
    completeWorkflow.mockResolvedValue(undefined);
    uploadMachinePhoto.mockResolvedValue(true);
    getDevis.mockResolvedValue([]);
    openDevis.mockResolvedValue(undefined);
  });

  describe("rendering + fetch initial", () => {
    it("affiche loader au mount", () => {
      const { UNSAFE_getByType } = render(<WorkflowInterventionScreen />);
      const { ActivityIndicator } = require("react-native");
      expect(UNSAFE_getByType(ActivityIndicator)).toBeTruthy();
    });

    it("affiche 'Aucune etape' si phases vide", async () => {
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText(/Aucune etape disponible/);
    });

    it("fetchAndStoreInterventionSteps throw -> continue avec local", async () => {
      fetchSteps.mockRejectedValue(new Error("net"));
      loadSteps.mockResolvedValue([step({ label: "Etape locale" })]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText("Etape locale");
    });

    it("groupe en phases avec indicateur de progression", async () => {
      loadSteps.mockResolvedValue([
        step({ id: "s1", metadata: { phase: "Phase A" } }),
        step({ id: "s2", metadata: { phase: "Phase B" } }),
      ]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText(/Phase 1 \/ 2/);
      await findByText("Phase A");
    });

    it("etape LISTE DEVIS : rend une ligne tappable qui ouvre le PDF du devis", async () => {
      getDevis.mockResolvedValue([
        { id: 77, numero_devis: "DV-2026-077", statut: "accepte" },
      ]);
      loadSteps.mockResolvedValue([
        step({
          label: "LISTE DERNIER DEVIS",
          metadata: { phase: "P1", input_kind: "display" },
        }),
      ]);
      const { findByText } = render(<WorkflowInterventionScreen />);

      await findByText(/#77 – DV-2026-077 \(accepte\)/);
      const button = await findByText("👁 Voir le devis");
      await act(async () => {
        fireEvent.press(button);
      });

      expect(openDevis).toHaveBeenCalledWith(77);
    });
  });

  describe("draft hydration", () => {
    it("hydrate depuis loadWorkflowDraft", async () => {
      loadSteps.mockResolvedValue([step()]);
      loadDraft.mockResolvedValue({
        inputValues: { s1: "DRAFTED" },
        commentValues: { s1: "cmt" },
        currentPhaseIdx: 0,
        sigTechData: "T",
        sigClientData: "C",
        sigClientPresent: false,
        showSignatureCard: false,
        updatedAt: "",
      });
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText("Etape");
    });

    it("draft avec showSignatureCard=true -> rend SignatureFinishCard", async () => {
      loadSteps.mockResolvedValue([step()]);
      loadDraft.mockResolvedValue({
        inputValues: {},
        commentValues: {},
        currentPhaseIdx: 0,
        sigTechData: "",
        sigClientData: "",
        sigClientPresent: true,
        showSignatureCard: true,
        updatedAt: "",
      });
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText("SignatureFinishCard");
    });

    it("draft null -> continue sans hydrater", async () => {
      loadSteps.mockResolvedValue([step()]);
      loadDraft.mockResolvedValue(null);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText("Etape");
    });

    it("hydrate partiellement quand draft incomplet", async () => {
      loadSteps.mockResolvedValue([step()]);
      loadDraft.mockResolvedValue({
        inputValues: null,
        commentValues: null,
        currentPhaseIdx: null,
        sigTechData: null,
        sigClientData: null,
        sigClientPresent: null,
        showSignatureCard: false,
        updatedAt: "",
      });
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText("Etape");
    });
  });

  describe("navigation phases", () => {
    it("Retour depuis phase 0 -> goBack", async () => {
      loadSteps.mockResolvedValue([step()]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText("Etape");
      await act(async () => {
        fireEvent.press(await findByText("Retour"));
      });
      expect(mockNav.goBack).toHaveBeenCalled();
    });

    it("Suivant valide phase et avance a phase suivante", async () => {
      loadSteps.mockResolvedValue([
        step({ id: "s1", metadata: { phase: "A" } }),
        step({ id: "s2", metadata: { phase: "B" } }),
      ]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText(/Phase 1 \/ 2/);
      await act(async () => {
        fireEvent.press(await findByText("Suivant →"));
      });
      await findByText(/Phase 2 \/ 2/);
    });

    it("Revenir depuis phase>0 -> phase precedente", async () => {
      loadSteps.mockResolvedValue([
        step({ id: "s1", metadata: { phase: "A" } }),
        step({ id: "s2", metadata: { phase: "B" } }),
      ]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText(/Phase 1/);
      await act(async () => {
        fireEvent.press(await findByText("Suivant →"));
      });
      await findByText(/Phase 2 \/ 2/);
      await act(async () => {
        fireEvent.press(await findByText("← Revenir"));
      });
      await findByText(/Phase 1 \/ 2/);
    });

    it("derniere phase + machineHasPhoto=true -> signature card directement", async () => {
      mockRoute.params = { interventionId: 5, brandColor: "#1E56A8", machineId: 10, machineHasPhoto: true };
      loadSteps.mockResolvedValue([step()]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText(/✅ Terminer la mission/);
      await act(async () => {
        fireEvent.press(await findByText(/✅ Terminer la mission/));
      });
      await findByText("SignatureFinishCard");
    });

    it("derniere phase + machineHasPhoto=false -> card photo machine", async () => {
      loadSteps.mockResolvedValue([step()]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText(/Phase 1/);
      await act(async () => {
        fireEvent.press(await findByText(/✅ Terminer la mission/));
      });
      await findByText(/Photo de la machine/);
    });
  });

  describe("savePhaseAndAdvance — validation", () => {
    it("bloque si step required sans valeur -> erreur", async () => {
      loadSteps.mockResolvedValue([
        step({ id: "s1", required: true, metadata: { phase: "A" } }),
      ]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText(/Phase 1/);
      await act(async () => {
        fireEvent.press(await findByText(/Terminer la mission/));
      });
      await findByText(/Merci de renseigner/);
    });

    it("bloque si validateStepValue retourne erreur", async () => {
      validateStep.mockReturnValue("Minimum: 5");
      loadSteps.mockResolvedValue([
        step({ id: "s1", type: "measurement", value: "0", metadata: { phase: "A" } }),
      ]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText(/Phase 1/);
      await act(async () => {
        fireEvent.press(await findByText(/Terminer la mission/));
      });
      await findByText(/valeurs sont invalides/);
    });

    it("bloque si commentaire requis manquant (probleme/panne)", async () => {
      loadSteps.mockResolvedValue([
        step({
          id: "s1",
          value: "probleme",
          metadata: { phase: "A", requires_comment: true },
        }),
      ]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText(/Phase 1/);
      await act(async () => {
        fireEvent.press(await findByText(/Terminer la mission/));
      });
      await findByText(/Commentaire obligatoire/);
    });

    it("skip autocalc steps de la validation", async () => {
      loadSteps.mockResolvedValue([
        step({
          id: "s1",
          required: true,
          value: null,
          metadata: { phase: "A", phase_text: "Calculs automatiques" },
        }),
      ]);
      loadSteps.mockResolvedValue([
        step({
          id: "s1",
          required: true,
          type: "text",
          value: "x",
          metadata: { phase: "Calculs automatiques" },
        }),
      ]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText(/Calculs automatiques/);
      await act(async () => {
        fireEvent.press(await findByText(/Terminer la mission/));
      });
      // Pas d'erreur bloquante
    });
  });

  describe("savePhaseAndAdvance — save online/offline", () => {
    it("online save via saveStepToServer", async () => {
      loadSteps.mockResolvedValue([
        step({ id: "s1", value: "v", metadata: { phase: "A" } }),
      ]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText(/Phase 1/);
      await act(async () => {
        fireEvent.press(await findByText(/Terminer la mission/));
      });
      await waitFor(() => expect(saveStep).toHaveBeenCalled());
    });

    it("offline -> enqueueSyncAction save_step", async () => {
      isOnline.mockResolvedValue(false);
      loadSteps.mockResolvedValue([
        step({ id: "s1", value: "v", metadata: { phase: "A" } }),
      ]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText(/Phase 1/);
      await act(async () => {
        fireEvent.press(await findByText(/Terminer la mission/));
      });
      await waitFor(() => expect(enqueue).toHaveBeenCalled());
    });

    it("save throw Error -> affiche message + bloque", async () => {
      saveStep.mockRejectedValue(new Error("net down"));
      loadSteps.mockResolvedValue([
        step({ id: "s1", value: "v", metadata: { phase: "A" } }),
      ]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText(/Phase 1/);
      await act(async () => {
        fireEvent.press(await findByText(/Terminer la mission/));
      });
      await findByText(/sauvegarde de la phase/);
    });

    it("save throw non-Error -> message fallback", async () => {
      saveStep.mockRejectedValue("str-err");
      loadSteps.mockResolvedValue([
        step({ id: "s1", value: "v", metadata: { phase: "A" } }),
      ]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText(/Phase 1/);
      await act(async () => {
        fireEvent.press(await findByText(/Terminer la mission/));
      });
      await findByText(/sauvegarde de la phase/);
    });

    it("skip display-only steps lors du save", async () => {
      loadSteps.mockResolvedValue([
        step({
          id: "s1",
          value: "x",
          metadata: { phase: "A", input_kind: "display" },
        }),
      ]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText(/Phase 1/);
      await act(async () => {
        fireEvent.press(await findByText(/Terminer la mission/));
      });
      // display-only skipped → saveStep not called
      await waitFor(() => expect(saveStep).not.toHaveBeenCalled());
    });

    it("skip photo steps lors du save (uploades separement)", async () => {
      loadSteps.mockResolvedValue([
        step({
          id: "s1",
          type: "photo",
          value: null,
          metadata: { phase: "A", input_kind: "photo" },
        }),
      ]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText(/Phase 1/);
      await act(async () => {
        fireEvent.press(await findByText(/Terminer la mission/));
      });
      await waitFor(() => expect(saveStep).not.toHaveBeenCalled());
    });

    it("skip step sans value ni comment", async () => {
      loadSteps.mockResolvedValue([
        step({ id: "s1", value: null, metadata: { phase: "A" } }),
      ]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText(/Phase 1/);
      await act(async () => {
        fireEvent.press(await findByText(/Terminer la mission/));
      });
      await waitFor(() => expect(saveStep).not.toHaveBeenCalled());
    });

    it("gate step auto-save (online)", async () => {
      loadSteps.mockResolvedValue([
        step({
          id: "s_gate",
          type: "text",
          label: "Commencer le rapport ?",
          metadata: { input_kind: "boolean" },
        }),
        step({ id: "s1", value: "v", metadata: { phase: "A" } }),
      ]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText(/Phase 1/);
      await act(async () => {
        fireEvent.press(await findByText(/Terminer la mission/));
      });
      await waitFor(() => {
        const calls = saveStep.mock.calls.filter((c) => c[0].stepKey === "s_gate");
        expect(calls.length).toBeGreaterThan(0);
      });
    });

    it("#2 : gate step auto-save HORS-LIGNE -> enqueue (pas perdu)", async () => {
      isOnline.mockResolvedValue(false);
      loadSteps.mockResolvedValue([
        step({
          id: "s_gate",
          type: "text",
          label: "Commencer le rapport ?",
          metadata: { input_kind: "boolean" },
          value: null,
        }),
        step({ id: "s1", value: "v", metadata: { phase: "A" } }),
      ]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText(/Phase 1/);
      await act(async () => {
        fireEvent.press(await findByText(/Terminer la mission/));
      });
      // Le gate doit etre ENFILE (save_step value "1"), pas envoye en direct.
      await waitFor(() => {
        const gateEnqueue = enqueue.mock.calls.find(
          (c) =>
            c[0]?.payload?.action === "save_step" &&
            c[0]?.payload?.step_key === "s_gate" &&
            c[0]?.payload?.value === "1"
        );
        expect(gateEnqueue).toBeDefined();
      });
      // ... et surtout pas via saveStepToServer (online)
      const direct = saveStep.mock.calls.filter((c) => c[0].stepKey === "s_gate");
      expect(direct.length).toBe(0);
    });

    it("gate step auto-save echec -> silent", async () => {
      saveStep.mockImplementation(async ({ stepKey }: { stepKey: string }) => {
        if (stepKey === "s_gate") throw new Error("gate-fail");
        return { version: 3, status: "in_progress" };
      });
      loadSteps.mockResolvedValue([
        step({
          id: "s_gate",
          type: "text",
          label: "Commencer ?",
          metadata: { input_kind: "boolean" },
          value: null,
        }),
        step({ id: "s1", value: "v", metadata: { phase: "A" } }),
      ]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText(/Phase 1/);
      await act(async () => {
        fireEvent.press(await findByText(/Terminer la mission/));
      });
      // Ne fait pas planter meme si gate save fail
    });

    it("skip gate deja rempli", async () => {
      loadSteps.mockResolvedValue([
        step({
          id: "s_gate",
          label: "Commencer le rapport ?",
          metadata: { input_kind: "boolean" },
          value: "1",
        }),
        step({ id: "s1", value: "v", metadata: { phase: "A" } }),
      ]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText(/Phase 1/);
      await act(async () => {
        fireEvent.press(await findByText(/Terminer la mission/));
      });
      await waitFor(() => {
        const calls = saveStep.mock.calls.filter((c) => c[0].stepKey === "s_gate");
        expect(calls.length).toBe(0);
      });
    });

    it("fetch steps refresh apres save throw -> silent", async () => {
      fetchSteps
        .mockResolvedValueOnce({ count: 0, workflowVersion: 2, workflowStatus: "in_progress" })
        .mockRejectedValueOnce(new Error("refresh-fail"));
      loadSteps.mockResolvedValue([
        step({ id: "s1", value: "v", metadata: { phase: "A" } }),
      ]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText(/Phase 1/);
      await act(async () => {
        fireEvent.press(await findByText(/Terminer la mission/));
      });
      // Pas de crash
    });
  });

  describe("Signature wizard (via mock)", () => {
    beforeEach(async () => {
      loadSteps.mockResolvedValue([step()]);
    });

    it("onCancel depuis SignatureFinishCard ferme la carte", async () => {
      mockRoute.params = { interventionId: 5, brandColor: "#1E56A8", machineId: 10, machineHasPhoto: true };
      const { findByText, findByTestId, queryByText } = render(<WorkflowInterventionScreen />);
      await findByText(/Phase 1/);
      await act(async () => {
        fireEvent.press(await findByText(/Terminer la mission/));
      });
      await findByText("SignatureFinishCard");
      await act(async () => {
        fireEvent.press(await findByTestId("sig-fire-cancel"));
      });
      expect(queryByText("SignatureFinishCard")).toBeNull();
    });

    it("onDraftChange met a jour state parent", async () => {
      mockRoute.params = { interventionId: 5, brandColor: "#1E56A8", machineId: 10, machineHasPhoto: true };
      const { findByText, findByTestId } = render(<WorkflowInterventionScreen />);
      await findByText(/Phase 1/);
      await act(async () => {
        fireEvent.press(await findByText(/Terminer la mission/));
      });
      await findByText("SignatureFinishCard");
      await act(async () => {
        fireEvent.press(await findByTestId("sig-fire-draft"));
      });
    });

    it("onConfirm online -> completeWorkflow + clearDraft + goBack", async () => {
      mockRoute.params = { interventionId: 5, brandColor: "#1E56A8", machineId: 10, machineHasPhoto: true };
      const { findByText, findByTestId } = render(<WorkflowInterventionScreen />);
      await findByText(/Phase 1/);
      await act(async () => {
        fireEvent.press(await findByText(/Terminer la mission/));
      });
      await act(async () => {
        fireEvent.press(await findByTestId("sig-fire-confirm"));
      });
      await waitFor(() => expect(completeWorkflow).toHaveBeenCalled());
      expect(clearDraft).toHaveBeenCalled();
      expect(mockNav.goBack).toHaveBeenCalled();
    });

    it("onConfirm offline -> enqueue complete_workflow", async () => {
      isOnline.mockResolvedValue(false);
      mockRoute.params = { interventionId: 5, brandColor: "#1E56A8", machineId: 10, machineHasPhoto: true };
      const { findByText, findByTestId } = render(<WorkflowInterventionScreen />);
      await findByText(/Phase 1/);
      await act(async () => {
        fireEvent.press(await findByText(/Terminer la mission/));
      });
      await act(async () => {
        fireEvent.press(await findByTestId("sig-fire-confirm"));
      });
      await waitFor(() =>
        expect(enqueue).toHaveBeenCalledWith(
          expect.objectContaining({ entityType: "intervention" })
        )
      );
    });

    it("onConfirm throw Error -> setFinishError", async () => {
      completeWorkflow.mockRejectedValue(new Error("fail-final"));
      mockRoute.params = { interventionId: 5, brandColor: "#1E56A8", machineId: 10, machineHasPhoto: true };
      const { findByText, findByTestId } = render(<WorkflowInterventionScreen />);
      await findByText(/Phase 1/);
      await act(async () => {
        fireEvent.press(await findByText(/Terminer la mission/));
      });
      await act(async () => {
        fireEvent.press(await findByTestId("sig-fire-confirm"));
      });
      // finishError remonte au SignatureFinishCard via errorMessage prop
      await waitFor(() => {
        expect(mockSubComps.signatureCardProps?.errorMessage).toBe("fail-final");
      });
    });

    it("onConfirm throw non-Error -> message fallback", async () => {
      completeWorkflow.mockRejectedValue("str-err");
      mockRoute.params = { interventionId: 5, brandColor: "#1E56A8", machineId: 10, machineHasPhoto: true };
      const { findByText, findByTestId } = render(<WorkflowInterventionScreen />);
      await findByText(/Phase 1/);
      await act(async () => {
        fireEvent.press(await findByText(/Terminer la mission/));
      });
      await act(async () => {
        fireEvent.press(await findByTestId("sig-fire-confirm"));
      });
      await waitFor(() => {
        expect(mockSubComps.signatureCardProps?.errorMessage).toMatch(/finalisation/);
      });
    });
  });

  describe("Machine photo card", () => {
    beforeEach(async () => {
      loadSteps.mockResolvedValue([step()]);
    });

    async function openMachinePhotoCard() {
      const { findByText, ...rest } = render(<WorkflowInterventionScreen />);
      await findByText(/Phase 1/);
      await act(async () => {
        fireEvent.press(await findByText(/Terminer la mission/));
      });
      await findByText(/Photo de la machine/);
      return { findByText, ...rest };
    }

    it("Passer -> va a signature directement", async () => {
      const { findByText, queryByText } = await openMachinePhotoCard();
      await act(async () => {
        fireEvent.press(await findByText("Passer →"));
      });
      await findByText("SignatureFinishCard");
      expect(queryByText(/Photo de la machine/)).toBeNull();
    });

    it("Ouvrir la camera -> permission deja accordee ouvre", async () => {
      const { findByText } = await openMachinePhotoCard();
      await act(async () => {
        fireEvent.press(await findByText(/Ouvrir la camera/));
      });
      await findByText(/Capturer/);
    });

    it("Ouvrir la camera -> permission refusee affiche message", async () => {
      mockCam.granted = false;
      mockCam.requestResult = { granted: false };
      const { findByText } = await openMachinePhotoCard();
      await act(async () => {
        fireEvent.press(await findByText(/Ouvrir la camera/));
      });
      await findByText(/Permission camera refusee/);
    });

    it("Ouvrir la camera -> permission accordee apres request", async () => {
      mockCam.granted = false;
      mockCam.requestResult = { granted: true };
      const { findByText } = await openMachinePhotoCard();
      await act(async () => {
        fireEvent.press(await findByText(/Ouvrir la camera/));
      });
      await findByText(/Capturer/);
    });

    it("Capturer -> upload online -> signature card", async () => {
      // base64:true -> la camera renvoie le base64 directement (plus de
      // fetch/Blob/FileReader). On verifie aussi que le data-URI prefixe
      // exige par le backend est bien transmis.
      mockCam.takePicture.mockResolvedValue({ uri: "file:///p.jpg", base64: "XYZ" });

      const { findByText } = await openMachinePhotoCard();
      await act(async () => {
        fireEvent.press(await findByText(/Ouvrir la camera/));
      });
      await act(async () => {
        fireEvent.press(await findByText(/Capturer/));
      });
      await waitFor(() =>
        expect(uploadMachinePhoto).toHaveBeenCalledWith(
          expect.anything(),
          "data:image/jpeg;base64,XYZ"
        )
      );
      await findByText("SignatureFinishCard");
    });

    it("Capturer -> la camera reste montee pendant la capture (anti ecran noir)", async () => {
      // Regression : avant le fix, passer saving=true demontait la CameraView
      // (branche loader prioritaire) -> takePictureAsync tournait sur une camera
      // arrachee -> ecran noir/blocage. On bloque la capture pour observer l'etat
      // saving + cameraOpen et verifier que la camera n'est PAS demontee.
      let resolveCapture: (v: { uri: string; base64: string }) => void = () => {};
      mockCam.takePicture.mockReturnValue(
        new Promise((r) => {
          resolveCapture = r;
        })
      );

      const { findByText, queryByTestId } = await openMachinePhotoCard();
      await act(async () => {
        fireEvent.press(await findByText(/Ouvrir la camera/));
      });
      expect(queryByTestId("machine-camera")).not.toBeNull();

      // Lance la capture sans la resoudre -> etat saving=true & cameraOpen=true.
      await act(async () => {
        fireEvent.press(await findByText(/Capturer/));
      });
      // La CameraView doit rester montee + le loader s'affiche par-dessus.
      expect(queryByTestId("machine-camera")).not.toBeNull();
      await findByText(/Enregistrement/);

      // On termine proprement la capture -> upload -> signature.
      await act(async () => {
        resolveCapture({ uri: "file:///p.jpg", base64: "XYZ" });
      });
      await waitFor(() => expect(uploadMachinePhoto).toHaveBeenCalled());
    });

    it("Capturer -> offline -> enqueue machine_photo", async () => {
      isOnline.mockResolvedValue(false);
      mockCam.takePicture.mockResolvedValue({ uri: "file:///p.jpg", base64: "XYZ" });

      const { findByText } = await openMachinePhotoCard();
      await act(async () => {
        fireEvent.press(await findByText(/Ouvrir la camera/));
      });
      await act(async () => {
        fireEvent.press(await findByText(/Capturer/));
      });
      await waitFor(() =>
        expect(enqueue).toHaveBeenCalledWith(
          expect.objectContaining({ entityType: "machine_photo" })
        )
      );
    });

    it("Capturer -> base64 manquant -> reset saving sans crash", async () => {
      mockCam.takePicture.mockResolvedValue({ uri: "file:///p.jpg", base64: null });
      const { findByText } = await openMachinePhotoCard();
      await act(async () => {
        fireEvent.press(await findByText(/Ouvrir la camera/));
      });
      await act(async () => {
        fireEvent.press(await findByText(/Capturer/));
      });
      // Pas de crash
    });

    it("Capturer -> takePicture throw Error -> affiche erreur", async () => {
      mockCam.takePicture.mockRejectedValue(new Error("cam-fail"));
      const { findByText } = await openMachinePhotoCard();
      await act(async () => {
        fireEvent.press(await findByText(/Ouvrir la camera/));
      });
      await act(async () => {
        fireEvent.press(await findByText(/Capturer/));
      });
      await findByText(/Erreur photo.*cam-fail/);
    });

    it("Capturer -> throw non-Error -> String()", async () => {
      mockCam.takePicture.mockRejectedValue("raw-err");
      const { findByText } = await openMachinePhotoCard();
      await act(async () => {
        fireEvent.press(await findByText(/Ouvrir la camera/));
      });
      await act(async () => {
        fireEvent.press(await findByText(/Capturer/));
      });
      await findByText(/Erreur photo.*raw-err/);
    });

    it("machineId=0 fallback sur interventionId pour entityId", async () => {
      mockRoute.params = { interventionId: 7, brandColor: "#1E56A8", machineId: 0, machineHasPhoto: false };
      isOnline.mockResolvedValue(false);
      mockCam.takePicture.mockResolvedValue({ uri: "file:///p.jpg", base64: "XYZ" });

      const { findByText } = await openMachinePhotoCard();
      await act(async () => {
        fireEvent.press(await findByText(/Ouvrir la camera/));
      });
      await act(async () => {
        fireEvent.press(await findByText(/Capturer/));
      });
      await waitFor(() =>
        expect(enqueue).toHaveBeenCalledWith(
          expect.objectContaining({ entityId: "7" })
        )
      );
    });
  });

  describe("StepInput — rendu par type", () => {
    it("display/section/info -> box d'info", async () => {
      loadSteps.mockResolvedValue([
        step({
          id: "s1",
          label: "Info step",
          metadata: { phase: "A", input_kind: "display", note: "Note serveur" },
        }),
      ]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText("Note serveur");
    });

    it("display sans note ni label -> vide", async () => {
      loadSteps.mockResolvedValue([
        step({
          id: "s1",
          label: "",
          metadata: { phase: "A", input_kind: "section" },
        }),
      ]);
      render(<WorkflowInterventionScreen />);
      await waitFor(() => expect(loadSteps).toHaveBeenCalled());
    });

    it("info kind -> rendu", async () => {
      loadSteps.mockResolvedValue([
        step({
          id: "s1",
          label: "Info label",
          metadata: { phase: "A", input_kind: "info" },
        }),
      ]);
      const { findAllByText } = render(<WorkflowInterventionScreen />);
      const matches = await findAllByText("Info label");
      expect(matches.length).toBeGreaterThan(0);
    });

    it("autocalc avec valeur -> affiche + unit", async () => {
      loadSteps.mockResolvedValue([
        step({
          id: "s1",
          value: "42.5",
          unit: "K",
          metadata: { phase: "Calculs automatiques", action_label: "t_condensation" },
        }),
      ]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText("42.5");
      await findByText("K");
    });

    it("autocalc sans valeur -> 'En attente'", async () => {
      loadSteps.mockResolvedValue([
        step({
          id: "s1",
          value: null,
          metadata: { phase: "Calculs automatiques" },
        }),
      ]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText(/En attente/);
    });

    it("autocalc sans unit -> pas de label unit", async () => {
      loadSteps.mockResolvedValue([
        step({
          id: "s1",
          value: "10",
          unit: null,
          metadata: { phase: "Calculs automatiques" },
        }),
      ]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText("10");
    });

    it("number kind -> TextInput keyboardType numeric", async () => {
      loadSteps.mockResolvedValue([
        step({
          id: "s1",
          value: null,
          unit: "C",
          metadata: { phase: "A", input_kind: "number" },
        }),
      ]);
      const { findAllByPlaceholderText } = render(<WorkflowInterventionScreen />);
      await findAllByPlaceholderText(/Valeur/);
    });

    it("number kind sans unit -> placeholder '0'", async () => {
      loadSteps.mockResolvedValue([
        step({
          id: "s1",
          value: null,
          metadata: { phase: "A", input_kind: "number" },
        }),
      ]);
      const { findAllByPlaceholderText } = render(<WorkflowInterventionScreen />);
      await findAllByPlaceholderText("0");
    });

    it("number kind avec value numerique -> stringifie", async () => {
      loadSteps.mockResolvedValue([
        step({
          id: "s1",
          value: 42 as unknown as string,
          metadata: { phase: "A", input_kind: "number" },
        }),
      ]);
      const { findAllByDisplayValue } = render(<WorkflowInterventionScreen />);
      await findAllByDisplayValue("42");
    });

    it("text kind -> TextInput Saisir...", async () => {
      loadSteps.mockResolvedValue([
        step({ id: "s1", value: null, metadata: { phase: "A", input_kind: "text" } }),
      ]);
      const { findByPlaceholderText } = render(<WorkflowInterventionScreen />);
      await findByPlaceholderText("Saisir...");
    });

    it("prerequisite non-CERFA -> TextInput Saisir", async () => {
      loadSteps.mockResolvedValue([
        step({
          id: "s1",
          label: "Autre prerequis",
          metadata: { phase: "A", input_kind: "prerequisite" },
        }),
      ]);
      const { findByPlaceholderText } = render(<WorkflowInterventionScreen />);
      await findByPlaceholderText("Saisir...");
    });

    it("prerequisite CERFA -> toggle + input conditionnel", async () => {
      loadSteps.mockResolvedValue([
        step({
          id: "s1",
          label: "CERFA 15497",
          metadata: { phase: "A", input_kind: "prerequisite" },
        }),
      ]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText(/Oui, preparer/);
    });

    it("prerequisite CERFA toggle active montre le TextInput reference", async () => {
      loadSteps.mockResolvedValue([
        step({
          id: "s1",
          label: "Prelevement huile",
          value: { enabled: true, reference: "ref-A" } as unknown as string,
          metadata: { phase: "A", input_kind: "prerequisite" },
        }),
      ]);
      const { findByDisplayValue } = render(<WorkflowInterventionScreen />);
      await findByDisplayValue("ref-A");
    });

    it("CERFA toggle press -> onChange enabled=true", async () => {
      loadSteps.mockResolvedValue([
        step({
          id: "s1",
          label: "CERFA",
          value: null,
          metadata: { phase: "A", input_kind: "prerequisite" },
        }),
      ]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      const toggle = await findByText(/Oui, preparer/);
      await act(async () => {
        fireEvent.press(toggle);
      });
      // Le toggle change d'etat sans crash
    });

    it("boolean kind -> OUI/NON options", async () => {
      loadSteps.mockResolvedValue([
        step({
          id: "s1",
          value: null,
          metadata: { phase: "A", input_kind: "boolean" },
        }),
      ]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText("OUI");
      await findByText("NON");
    });

    it("boolean press OUI -> onChange '1'", async () => {
      loadSteps.mockResolvedValue([
        step({
          id: "s1",
          value: null,
          metadata: { phase: "A", input_kind: "boolean" },
        }),
      ]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText(/Phase 1/);
      await act(async () => {
        fireEvent.press(await findByText("OUI"));
      });
    });

    it("boolean press NON -> onChange '0'", async () => {
      loadSteps.mockResolvedValue([
        step({
          id: "s1",
          value: null,
          metadata: { phase: "A", input_kind: "boolean" },
        }),
      ]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText(/Phase 1/);
      await act(async () => {
        fireEvent.press(await findByText("NON"));
      });
    });

    it("autocalc value objet -> JSON.stringify", async () => {
      loadSteps.mockResolvedValue([
        step({
          id: "s1",
          value: { x: 1 } as unknown as string,
          metadata: { phase: "Calculs automatiques" },
        }),
      ]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText(/\{"x":1\}/);
    });

    it("boolean value=true/oui/1 -> OUI actif", async () => {
      loadSteps.mockResolvedValue([
        step({
          id: "s1",
          value: "true",
          metadata: { phase: "A", input_kind: "boolean" },
        }),
      ]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText("OUI");
    });

    it("boolean value=0/false/non -> NON actif", async () => {
      loadSteps.mockResolvedValue([
        step({
          id: "s1",
          value: "non",
          metadata: { phase: "A", input_kind: "boolean" },
        }),
      ]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText("NON");
    });

    it("choice kind avec options", async () => {
      loadSteps.mockResolvedValue([
        step({
          id: "s1",
          value: null,
          options: ["OK", "Probleme"],
          metadata: { phase: "A", input_kind: "choice" },
        }),
      ]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText("OK");
      await findByText("Probleme");
    });

    it("choice press option -> onChange", async () => {
      loadSteps.mockResolvedValue([
        step({
          id: "s1",
          value: null,
          options: ["OK", "Probleme"],
          metadata: { phase: "A", input_kind: "choice" },
        }),
      ]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText(/Phase 1/);
      await act(async () => {
        fireEvent.press(await findByText("Probleme"));
      });
    });

    it("choice sans options -> 'Aucune option'", async () => {
      loadSteps.mockResolvedValue([
        step({
          id: "s1",
          value: null,
          options: [],
          metadata: { phase: "A", input_kind: "choice" },
        }),
      ]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText(/Aucune option disponible/);
    });

    it("choice options non-array -> 'Aucune option'", async () => {
      loadSteps.mockResolvedValue([
        step({
          id: "s1",
          value: null,
          options: undefined,
          metadata: { phase: "A", input_kind: "choice" },
        }),
      ]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText(/Aucune option disponible/);
    });

    it("choice avec valeur selectionnee -> isActive", async () => {
      loadSteps.mockResolvedValue([
        step({
          id: "s1",
          value: "OK",
          options: ["OK", "Probleme"],
          metadata: { phase: "A", input_kind: "choice" },
        }),
      ]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText("OK");
    });

    it("photo kind -> CameraCapture rendu", async () => {
      loadSteps.mockResolvedValue([
        step({
          id: "s1",
          type: "photo",
          value: [],
          metadata: { phase: "A", input_kind: "photo", workflow_step_id: 99 },
        }),
      ]);
      const { findByTestId } = render(<WorkflowInterventionScreen />);
      await findByTestId("cam-capture");
    });

    it("photo capture -> ajoute dans onChange avec mime png", async () => {
      loadSteps.mockResolvedValue([
        step({
          id: "s1",
          type: "photo",
          value: [],
          metadata: { phase: "A", input_kind: "photo", workflow_step_id: 99 },
        }),
      ]);
      const { findByTestId, findByText } = render(<WorkflowInterventionScreen />);
      await findByText(/Phase 1/);
      await act(async () => {
        fireEvent.press(await findByTestId("cam-fire"));
      });
    });

    it("photo liste -> affichage preview + suppression", async () => {
      loadSteps.mockResolvedValue([
        step({
          id: "s1",
          type: "photo",
          value: [{ id: "1", uri: "file:///x.jpg", name: "x.jpg" }] as unknown as string,
          metadata: { phase: "A", input_kind: "photo" },
        }),
      ]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText("x.jpg");
      await act(async () => {
        fireEvent.press(await findByText("Supprimer"));
      });
    });

    it("photo sans nom fallback pop URI", async () => {
      loadSteps.mockResolvedValue([
        step({
          id: "s1",
          type: "photo",
          value: [{ id: "1", uri: "file:///noname.jpg" }] as unknown as string,
          metadata: { phase: "A", input_kind: "photo" },
        }),
      ]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText("noname.jpg");
    });

    it("photo avec uploaded=true -> 'Synchronisee'", async () => {
      loadSteps.mockResolvedValue([
        step({
          id: "s1",
          type: "photo",
          value: [{ id: "1", uri: "file:///a.jpg", uploaded: true }] as unknown as string,
          metadata: { phase: "A", input_kind: "photo" },
        }),
      ]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText(/Synchronisee/);
    });

    it("signature kind -> SignatureCapture rendu", async () => {
      loadSteps.mockResolvedValue([
        step({
          id: "s1",
          type: "signature",
          value: null,
          metadata: { phase: "A" },
        }),
      ]);
      const { findByTestId } = render(<WorkflowInterventionScreen />);
      await findByTestId("sig-capture");
    });

    it("signature capture -> onChange avec StepSignatureValue", async () => {
      loadSteps.mockResolvedValue([
        step({
          id: "s1",
          type: "signature",
          value: null,
          metadata: { phase: "A" },
        }),
      ]);
      const { findByTestId, findByText } = render(<WorkflowInterventionScreen />);
      await findByText(/Phase 1/);
      await act(async () => {
        fireEvent.press(await findByTestId("sig-capture-fire"));
      });
    });

    it("signature avec valeur -> preview + effacer", async () => {
      loadSteps.mockResolvedValue([
        step({
          id: "s1",
          type: "signature",
          value: { signed: true, signed_by: "technicien", uri: "file:///s.png" } as unknown as string,
          metadata: { phase: "A" },
        }),
      ]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText(/Phase 1/);
      await act(async () => {
        fireEvent.press(await findByText("Effacer signature"));
      });
    });

    it("type non supporte -> message unsupported", async () => {
      loadSteps.mockResolvedValue([
        step({
          id: "s1",
          type: "checklist",
          value: null,
          metadata: { phase: "A", input_kind: "unknown-kind" },
        }),
      ]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText(/non pris en charge/);
    });

    it("workflow_step_id null metadata pour photo", async () => {
      loadSteps.mockResolvedValue([
        step({
          id: "s1",
          type: "photo",
          value: [],
          metadata: { phase: "A", input_kind: "photo" },
        }),
      ]);
      const { findByTestId } = render(<WorkflowInterventionScreen />);
      await findByTestId("cam-capture");
    });
  });

  describe("finishing state", () => {
    it("finishing=true affiche ActivityIndicator sur bouton Suivant", async () => {
      completeWorkflow.mockImplementation(
        () => new Promise(() => {})
      );
      mockRoute.params = { interventionId: 5, brandColor: "#1E56A8", machineId: 10, machineHasPhoto: true };
      loadSteps.mockResolvedValue([step()]);
      const { findByText, findByTestId, UNSAFE_getAllByType } = render(
        <WorkflowInterventionScreen />
      );
      await findByText(/Phase 1/);
      await act(async () => {
        fireEvent.press(await findByText(/Terminer la mission/));
      });
      await act(async () => {
        fireEvent.press(await findByTestId("sig-fire-confirm"));
      });
      const { ActivityIndicator } = require("react-native");
      await waitFor(() => {
        const indicators = UNSAFE_getAllByType(ActivityIndicator);
        expect(indicators.length).toBeGreaterThan(0);
      });
    });
  });

  describe("phases === 0 — bouton disabled", () => {
    it("phases vides -> bouton Suivant disabled", async () => {
      loadSteps.mockResolvedValue([]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText(/Aucune etape disponible/);
      await findByText(/Suivant/);
    });
  });

  describe("extension step rendering", () => {
    it("rend le badge '↳ Sous-etape' pour is_extension", async () => {
      loadSteps.mockResolvedValue([
        step({
          id: "parent",
          value: "probleme",
          metadata: { phase: "A", input_kind: "choice" },
          options: ["OK", "probleme"],
        }),
        step({
          id: "ext",
          metadata: { phase: "A", is_extension: true },
        }),
      ]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText(/Sous-etape/);
    });
  });

  describe("required mark", () => {
    it("step required -> affiche *", async () => {
      loadSteps.mockResolvedValue([
        step({ id: "s1", label: "ReqStep", required: true, metadata: { phase: "A" } }),
      ]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText(/Phase 1/);
      await findByText(/ReqStep/);
    });
  });

  describe("onChangeText callbacks manquants", () => {
    it("number input onChangeText propage", async () => {
      loadSteps.mockResolvedValue([
        step({ id: "s1", value: null, metadata: { phase: "A", input_kind: "number" } }),
      ]);
      const { findByPlaceholderText } = render(<WorkflowInterventionScreen />);
      const input = await findByPlaceholderText("0");
      await act(async () => {
        fireEvent.changeText(input, "42");
      });
    });

    it("text input onChangeText propage", async () => {
      loadSteps.mockResolvedValue([
        step({ id: "s1", value: null, metadata: { phase: "A", input_kind: "text" } }),
      ]);
      const { findByPlaceholderText } = render(<WorkflowInterventionScreen />);
      const input = await findByPlaceholderText("Saisir...");
      await act(async () => {
        fireEvent.changeText(input, "hello");
      });
    });

    it("CERFA reference input onChangeText propage", async () => {
      loadSteps.mockResolvedValue([
        step({
          id: "s1",
          label: "CERFA",
          value: { enabled: true, reference: "" },
          metadata: { phase: "A", input_kind: "prerequisite" },
        }),
      ]);
      const { findByPlaceholderText } = render(<WorkflowInterventionScreen />);
      const input = await findByPlaceholderText(/numero/);
      await act(async () => {
        fireEvent.changeText(input, "XYZ");
      });
    });

    it("extension step non visible -> skip dans validation et save", async () => {
      loadSteps.mockResolvedValue([
        step({
          id: "parent",
          value: "OK",
          metadata: { phase: "A", input_kind: "choice" },
          options: ["OK", "probleme"],
        }),
        step({
          id: "ext",
          required: true,
          metadata: { phase: "A", is_extension: true },
        }),
      ]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText(/Phase 1/);
      // L'extension n'est pas visible car parent=OK, donc pas bloquante
      await act(async () => {
        fireEvent.press(await findByText(/Terminer la mission/));
      });
    });

    it("save phase et avance: persistance draft declenche setTimeout apres hydratation", async () => {
      jest.useFakeTimers();
      loadSteps.mockResolvedValue([step()]);
      loadDraft.mockResolvedValue(null);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText(/Phase 1/);
      // Avancer les timers pour declencher le setTimeout(400) de persist
      await act(async () => {
        jest.advanceTimersByTime(500);
      });
      expect(saveDraft).toHaveBeenCalled();
      jest.useRealTimers();
    });

    // M5 : flush immediat au passage en arriere-plan (sans attendre le debounce
    // 400ms), pour ne pas perdre la derniere frappe si l'app est tuee.
    it("passage en arriere-plan -> flush immediat du draft (sans debounce)", async () => {
      const { AppState } = require("react-native");
      const handlers: Array<(s: string) => void> = [];
      // On remplace puis RESTAURE la reference d'origine a la main : mockRestore()
      // restaurerait une impl qui renvoie undefined -> le cleanup sub.remove()
      // des tests suivants planterait.
      const original = AppState.addEventListener;
      AppState.addEventListener = jest.fn((...args: unknown[]) => {
        handlers.push(args[1] as (s: string) => void);
        return { remove: jest.fn() };
      });

      try {
        loadSteps.mockResolvedValue([step()]);
        loadDraft.mockResolvedValue(null);
        const { findByText } = render(<WorkflowInterventionScreen />);
        await findByText(/Phase 1/);

        saveDraft.mockClear();
        await act(async () => {
          handlers.forEach((h) => h("background"));
        });

        expect(saveDraft).toHaveBeenCalled();
      } finally {
        AppState.addEventListener = original;
      }
    });
  });

  describe("comment visible / required", () => {
    it("requires_comment=true + probleme -> label obligatoire", async () => {
      loadSteps.mockResolvedValue([
        step({
          id: "s1",
          value: "probleme",
          metadata: { phase: "A", requires_comment: true },
        }),
      ]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText(/obligatoire/);
    });

    it("requires_comment=true + ok -> label simple", async () => {
      loadSteps.mockResolvedValue([
        step({
          id: "s1",
          value: "ok",
          metadata: { phase: "A", requires_comment: true },
        }),
      ]);
      const { findByText } = render(<WorkflowInterventionScreen />);
      await findByText(/Commentaire \(si Probleme\/Panne\)/);
    });

    it("change commentaire propage dans commentValues", async () => {
      loadSteps.mockResolvedValue([
        step({
          id: "s1",
          value: "ok",
          metadata: { phase: "A", requires_comment: true },
        }),
      ]);
      const { findByPlaceholderText } = render(<WorkflowInterventionScreen />);
      const ta = await findByPlaceholderText(/Probleme\/Panne/);
      await act(async () => {
        fireEvent.changeText(ta, "mon cmt");
      });
    });
  });
});
