jest.mock("../../src/services/apiClient", () => ({
  __esModule: true,
  default: { get: jest.fn(), post: jest.fn() },
}));
jest.mock("../../src/core/localDatabase", () => ({
  applyServerStepsDelta: jest.fn(),
  getInterventionDetailById: jest.fn(),
  getInterventionsSnapshot: jest.fn(),
  saveInterventionDetail: jest.fn(),
  saveInterventionsSnapshot: jest.fn(),
}));
jest.mock("../../src/core/environmentService", () => ({
  requireEnvironmentBaseUrl: jest.fn(),
}));
jest.mock("../../src/services/tokenStorage", () => ({
  readSession: jest.fn(),
}));
jest.mock("../../src/services/syncService", () => ({
  enqueueOfflineAction: jest.fn(),
}));

import apiClient from "../../src/services/apiClient";
import * as localDatabase from "../../src/core/localDatabase";
import * as environmentService from "../../src/core/environmentService";
import * as tokenStorage from "../../src/services/tokenStorage";
import * as syncService from "../../src/services/syncService";
import {
  completeMobileWorkflow,
  enqueueWorkflowStepPhoto,
  fetchAndStoreInterventionSteps,
  prefetchWorkflows,
  getInterventionById,
  getInterventionPrecheck,
  getMyInterventions,
  saveInterventionPrecheck,
  saveStepToServer,
  startIntervention,
  uploadMachinePhoto,
  uploadWorkflowStepPhoto,
} from "../../src/services/interventionsApi";

const get = apiClient.get as jest.Mock;
const post = apiClient.post as jest.Mock;
const saveSnap = localDatabase.saveInterventionsSnapshot as jest.Mock;
const getSnap = localDatabase.getInterventionsSnapshot as jest.Mock;
const saveDetail = localDatabase.saveInterventionDetail as jest.Mock;
const getDetail = localDatabase.getInterventionDetailById as jest.Mock;
const applyDelta = localDatabase.applyServerStepsDelta as jest.Mock;
const requireBase = environmentService.requireEnvironmentBaseUrl as jest.Mock;
const readSession = tokenStorage.readSession as jest.Mock;
const enqueue = syncService.enqueueOfflineAction as jest.Mock;

describe("interventionsApi", () => {
  beforeEach(() => {
    jest.clearAllMocks();
    requireBase.mockResolvedValue("https://api.test");
    readSession.mockResolvedValue({ accessToken: "tok", refreshToken: null, user: null });
  });

  describe("getMyInterventions", () => {
    it("retourne la payload + persist la snapshot", async () => {
      const items = [{ id: 1 }];
      get.mockResolvedValue({
        data: {
          success: true,
          data: { items, count: 1, technicien_id: 2, date_debut: "d1", date_fin: "d2" },
        },
      });
      const res = await getMyInterventions({ date_debut: "x" });
      expect(res.items).toEqual(items);
      expect(res.count).toBe(1);
      expect(res.technicien_id).toBe(2);
      expect(saveSnap).toHaveBeenCalledWith(items);
    });

    it("retombe sur count=items.length quand count non fini", async () => {
      get.mockResolvedValue({
        data: { success: true, data: { items: [{ id: 1 }, { id: 2 }], count: "bad" } },
      });
      const res = await getMyInterventions();
      expect(res.count).toBe(2);
    });

    it("retombe items=[] quand payload.items non-array", async () => {
      get.mockResolvedValue({ data: { success: true, data: { items: null } } });
      const res = await getMyInterventions();
      expect(res.items).toEqual([]);
    });

    it("fallback local quand HTTP echoue", async () => {
      get.mockRejectedValue(new Error("offline"));
      getSnap.mockResolvedValue({
        items: [{ id: 99 }],
        count: 1,
        updated_at: "2026-01-01",
      });
      const res = await getMyInterventions();
      expect(res.items).toEqual([{ id: 99 }]);
    });

    it("rethrow si local vide", async () => {
      get.mockRejectedValue(new Error("offline"));
      getSnap.mockResolvedValue({ items: [], count: 0, updated_at: "2026-01-01" });
      await expect(getMyInterventions()).rejects.toThrow("offline");
    });

    it("leve message serveur si success=false", async () => {
      get.mockResolvedValue({ data: { success: false, message: "server-err" } });
      getSnap.mockResolvedValue({ items: [], count: 0, updated_at: "2026-01-01" });
      await expect(getMyInterventions()).rejects.toThrow("server-err");
    });

    it("message par defaut si success=false sans message", async () => {
      get.mockResolvedValue({ data: { success: false } });
      getSnap.mockResolvedValue({ items: [], count: 0, updated_at: "2026-01-01" });
      await expect(getMyInterventions()).rejects.toThrow("Unable to fetch interventions");
    });
  });

  describe("getInterventionById", () => {
    it("retourne item et prefetch les steps", async () => {
      const item = { id: 5, description: "x" };
      get
        .mockResolvedValueOnce({ data: { success: true, data: { item } } })
        .mockResolvedValueOnce({
          data: { success: true, data: { items: [{ id: "s1" } as unknown], workflow: {} } },
        });
      applyDelta.mockResolvedValue(1);
      const res = await getInterventionById(5);
      expect(res).toEqual(item);
      expect(saveDetail).toHaveBeenCalledWith(item);
    });

    it("ignore les erreurs du prefetch steps (best-effort)", async () => {
      const item = { id: 5 };
      get
        .mockResolvedValueOnce({ data: { success: true, data: { item } } })
        .mockRejectedValueOnce(new Error("steps-fail"));
      const res = await getInterventionById(5);
      expect(res).toEqual(item);
    });

    it("fallback local quand HTTP echoue", async () => {
      get.mockRejectedValue(new Error("offline"));
      getDetail.mockResolvedValue({ item: { id: 5 }, updated_at: "x" });
      const res = await getInterventionById(5);
      expect(res).toEqual({ id: 5 });
    });

    it("rethrow si local absent", async () => {
      get.mockRejectedValue(new Error("offline"));
      getDetail.mockResolvedValue(null);
      await expect(getInterventionById(5)).rejects.toThrow("offline");
    });

    it("leve message serveur si success=false", async () => {
      get.mockResolvedValue({ data: { success: false, message: "nope" } });
      getDetail.mockResolvedValue(null);
      await expect(getInterventionById(5)).rejects.toThrow("nope");
    });

    it("message par defaut si success mais pas d'item", async () => {
      get.mockResolvedValue({ data: { success: true, data: {} } });
      getDetail.mockResolvedValue(null);
      await expect(getInterventionById(5)).rejects.toThrow("Unable to fetch intervention");
    });
  });

  describe("fetchAndStoreInterventionSteps", () => {
    it("retourne count + version + status quand succes", async () => {
      get.mockResolvedValue({
        data: {
          success: true,
          data: { items: [{ id: "s1" }], workflow: { version: 3, status: "done" } },
        },
      });
      applyDelta.mockResolvedValue(1);
      const res = await fetchAndStoreInterventionSteps(5);
      expect(res).toEqual({ count: 1, workflowVersion: 3, workflowStatus: "done" });
      expect(applyDelta).toHaveBeenCalledWith([{ id: "s1" }]);
    });

    it("count=0 quand pas de steps", async () => {
      get.mockResolvedValue({ data: { success: true, data: { items: [] } } });
      const res = await fetchAndStoreInterventionSteps(5);
      expect(res.count).toBe(0);
      expect(applyDelta).not.toHaveBeenCalled();
    });

    it("retourne zero quand reponse invalide", async () => {
      get.mockResolvedValue({ data: { success: false } });
      const res = await fetchAndStoreInterventionSteps(5);
      expect(res).toEqual({ count: 0, workflowVersion: 0, workflowStatus: "unknown" });
    });

    it("valeurs par defaut workflow", async () => {
      get.mockResolvedValue({
        data: { success: true, data: { items: [{ id: "s1" }] } },
      });
      applyDelta.mockResolvedValue(1);
      const res = await fetchAndStoreInterventionSteps(5);
      expect(res.workflowVersion).toBe(0);
      expect(res.workflowStatus).toBe("in_progress");
    });
  });

  describe("uploadWorkflowStepPhoto", () => {
    const args = {
      workflowStepId: 12,
      photoUri: "file:///x.jpg",
      photoName: "p.jpg",
      mimeType: "image/jpeg",
      idempotencyKey: "abc",
    };

    beforeEach(() => {
      (globalThis as unknown as { fetch: jest.Mock }).fetch = jest.fn();
      // RN FormData polyfill minimal
      (globalThis as unknown as { FormData: typeof FormData }).FormData = class {
        private entries: Array<[string, unknown]> = [];
        append(key: string, value: unknown) {
          this.entries.push([key, value]);
        }
      } as unknown as typeof FormData;
    });

    it("leve si pas de session", async () => {
      readSession.mockResolvedValue({ accessToken: null, refreshToken: null, user: null });
      await expect(uploadWorkflowStepPhoto(args)).rejects.toThrow("Session expiree");
    });

    it("retourne data.data quand succes", async () => {
      (globalThis.fetch as jest.Mock).mockResolvedValue({
        ok: true,
        status: 200,
        json: async () => ({ success: true, data: { id: 77 } }),
      });
      expect(await uploadWorkflowStepPhoto(args)).toEqual({ id: 77 });
    });

    it("retourne null si data absent", async () => {
      (globalThis.fetch as jest.Mock).mockResolvedValue({
        ok: true,
        status: 200,
        json: async () => ({ success: true }),
      });
      expect(await uploadWorkflowStepPhoto(args)).toBeNull();
    });

    it("leve message serveur si !success", async () => {
      (globalThis.fetch as jest.Mock).mockResolvedValue({
        ok: true,
        status: 200,
        json: async () => ({ success: false, message: "bad" }),
      });
      await expect(uploadWorkflowStepPhoto(args)).rejects.toThrow("bad");
    });

    it("leve HTTP status si parse json echoue", async () => {
      (globalThis.fetch as jest.Mock).mockResolvedValue({
        ok: false,
        status: 500,
        json: async () => {
          throw new Error("p");
        },
      });
      await expect(uploadWorkflowStepPhoto(args)).rejects.toThrow("Upload photo echoue (HTTP 500)");
    });
  });

  describe("prefetchWorkflows", () => {
    it("precharge detail + etapes de chaque id, best-effort, ignore les echecs", async () => {
      applyDelta.mockResolvedValue(1);
      // Pas de detail local -> un echec sur intervention.php se propage (rethrow)
      // et l'id est ignore au lieu d'etre compte via le fallback offline.
      getDetail.mockResolvedValue(null);
      get
        // id 1 OK : detail (intervention.php) puis etapes (intervention_steps.php)
        .mockResolvedValueOnce({ data: { success: true, data: { item: { id: 1 } } } })
        .mockResolvedValueOnce({
          data: { success: true, data: { items: [{}], workflow: { version: 2, status: "in_progress" } } },
        })
        // id 2 : detail echoue -> ignore (pas d'appel etapes)
        .mockRejectedValueOnce(new Error("network"))
        // id 3 OK : detail puis etapes
        .mockResolvedValueOnce({ data: { success: true, data: { item: { id: 3 } } } })
        .mockResolvedValueOnce({ data: { success: true, data: { items: [{}], workflow: {} } } });
      const n = await prefetchWorkflows([1, 2, 3]);
      expect(n).toBe(2); // 2 prechargees, l'echec n'interrompt pas la boucle
      expect(saveDetail).toHaveBeenCalledTimes(2); // detail cache pour offline
      expect(get).toHaveBeenCalledTimes(5); // 2 (id1) + 1 (id2) + 2 (id3)
    });

    it("liste vide -> 0, aucun appel", async () => {
      const n = await prefetchWorkflows([]);
      expect(n).toBe(0);
      expect(get).not.toHaveBeenCalled();
    });
  });

  describe("enqueueWorkflowStepPhoto", () => {
    it("met en file un payload multipart compatible syncService", async () => {
      enqueue.mockResolvedValue(undefined);
      await enqueueWorkflowStepPhoto({
        workflowStepId: 12,
        photoUri: "file:///perm.jpg",
        photoName: "p.jpg",
        mimeType: "image/jpeg",
        idempotencyKey: "abc",
      });
      expect(enqueue).toHaveBeenCalledWith({
        endpoint: "/mobile/intervention_workflow_files.php",
        method: "POST",
        payload: {
          __multipart: true,
          workflow_step_id: 12,
          idempotency_key: "abc",
          upload_files: [
            {
              field: "file",
              uri: "file:///perm.jpg",
              name: "p.jpg",
              type: "image/jpeg",
            },
          ],
        },
        entityType: "workflow_photo",
        entityId: "12",
      });
    });
  });

  describe("saveStepToServer", () => {
    it("retourne version + status quand succes", async () => {
      post.mockResolvedValue({
        data: { success: true, data: { workflow: { version: 4, status: "ok" } } },
      });
      const res = await saveStepToServer({
        interventionId: 1,
        stepKey: "k",
        value: "v",
        comment: null,
        version: 3,
        idempotencyKey: "id",
      });
      expect(res).toEqual({ version: 4, status: "ok" });
    });

    it("version fallback si non fournie", async () => {
      post.mockResolvedValue({ data: { success: true, data: {} } });
      const res = await saveStepToServer({
        interventionId: 1,
        stepKey: "k",
        value: "v",
        comment: null,
        version: 3,
        idempotencyKey: "id",
      });
      expect(res.version).toBe(3);
      expect(res.status).toBe("in_progress");
    });

    it("leve message serveur si echec", async () => {
      post.mockResolvedValue({ data: { success: false, message: "x" } });
      await expect(
        saveStepToServer({
          interventionId: 1,
          stepKey: "k",
          value: "v",
          comment: null,
          version: 3,
          idempotencyKey: "id",
        })
      ).rejects.toThrow("x");
    });

    it("message par defaut si echec sans message", async () => {
      post.mockResolvedValue({ data: { success: false } });
      await expect(
        saveStepToServer({
          interventionId: 1,
          stepKey: "k",
          value: "v",
          comment: null,
          version: 3,
          idempotencyKey: "id",
        })
      ).rejects.toThrow("Echec save_step");
    });
  });

  describe("uploadMachinePhoto", () => {
    it("retourne true si success", async () => {
      post.mockResolvedValue({ data: { success: true } });
      expect(await uploadMachinePhoto(1, "b64")).toBe(true);
    });

    it("retourne false sinon", async () => {
      post.mockResolvedValue({ data: { success: false } });
      expect(await uploadMachinePhoto(1, "b64")).toBe(false);
    });
  });

  describe("completeMobileWorkflow", () => {
    const sigs = { signature_technicien: "t", signature_client: "c", client_present: true };

    it("succes : ne leve pas", async () => {
      post.mockResolvedValue({ data: { success: true } });
      await expect(completeMobileWorkflow(1, 2, sigs)).resolves.toBeUndefined();
    });

    it("echec sans errors : leve baseMsg", async () => {
      post.mockResolvedValue({ data: { success: false, message: "boom" } });
      await expect(completeMobileWorkflow(1, 2, sigs)).rejects.toThrow("boom");
    });

    it("echec sans message : 'Echec de la finalisation'", async () => {
      post.mockResolvedValue({ data: { success: false } });
      await expect(completeMobileWorkflow(1, 2, sigs)).rejects.toThrow(
        "Echec de la finalisation"
      );
    });

    it("echec avec errors : formatte detail (< 5)", async () => {
      post.mockResolvedValue({
        data: {
          success: false,
          message: "err",
          errors: [
            { item: "Etape A", errors: ["manque A"] },
            { step_key: "kB", errors: ["manque B"] },
          ],
        },
      });
      await expect(completeMobileWorkflow(1, 2, sigs)).rejects.toThrow(/Etape A.*manque A/);
    });

    it("echec avec errors : ajoute 'et N autre(s)' si > 5", async () => {
      post.mockResolvedValue({
        data: {
          success: false,
          message: "err",
          errors: Array.from({ length: 7 }).map((_, i) => ({
            item: `E${i}`,
            errors: [`m${i}`],
          })),
        },
      });
      await expect(completeMobileWorkflow(1, 2, sigs)).rejects.toThrow(/2 autre\(s\)/);
    });

    it("echec avec errors sans item ni step_key : '?' + 'invalide'", async () => {
      post.mockResolvedValue({
        data: {
          success: false,
          message: "err",
          errors: [{ errors: null }],
        },
      });
      await expect(completeMobileWorkflow(1, 2, sigs)).rejects.toThrow(/\?: invalide/);
    });
  });

  describe("getInterventionPrecheck", () => {
    it("retourne data", async () => {
      get.mockResolvedValue({ data: { success: true, data: { foo: 1 } } });
      expect(await getInterventionPrecheck(5)).toEqual({ foo: 1 });
    });

    it("leve si success=false", async () => {
      get.mockResolvedValue({ data: { success: false, message: "x" } });
      await expect(getInterventionPrecheck(5)).rejects.toThrow("x");
    });

    it("message par defaut si success=true mais data absent", async () => {
      get.mockResolvedValue({ data: { success: true } });
      await expect(getInterventionPrecheck(5)).rejects.toThrow("Unable to fetch precheck");
    });
  });

  describe("saveInterventionPrecheck", () => {
    const payload = { intervention_id: 5, cerfa_enabled: true } as unknown as Parameters<
      typeof saveInterventionPrecheck
    >[0];

    it("retourne data quand succes", async () => {
      post.mockResolvedValue({ data: { success: true, data: { foo: 1 } } });
      expect(await saveInterventionPrecheck(payload)).toEqual({ foo: 1 });
    });

    it("enqueue offline et rethrow quand echec", async () => {
      post.mockRejectedValue(new Error("offline"));
      await expect(saveInterventionPrecheck(payload)).rejects.toThrow("offline");
      expect(enqueue).toHaveBeenCalledWith(
        expect.objectContaining({
          endpoint: "/mobile/intervention_precheck.php",
          method: "POST",
          entityType: "precheck",
          entityId: "5",
        })
      );
    });

    it("enqueue + rethrow quand success=false", async () => {
      post.mockResolvedValue({ data: { success: false, message: "nope" } });
      await expect(saveInterventionPrecheck(payload)).rejects.toThrow("nope");
      expect(enqueue).toHaveBeenCalled();
    });

    it("rejette Unable to save precheck si success true mais data absent", async () => {
      post.mockResolvedValue({ data: { success: true } });
      await expect(saveInterventionPrecheck(payload)).rejects.toThrow(
        "Unable to save precheck"
      );
    });
  });

  describe("startIntervention", () => {
    const payload = { intervention_id: 5, cerfa_enabled: false } as unknown as Parameters<
      typeof startIntervention
    >[0];

    it("retourne data quand succes", async () => {
      post.mockResolvedValue({ data: { success: true, data: { ok: true } } });
      expect(await startIntervention(payload)).toEqual({ ok: true });
    });

    it("enqueue + rethrow quand echec", async () => {
      post.mockRejectedValue(new Error("net"));
      await expect(startIntervention(payload)).rejects.toThrow("net");
      expect(enqueue).toHaveBeenCalledWith(
        expect.objectContaining({
          entityType: "intervention_start",
        })
      );
    });

    it("message par defaut si success=false sans message", async () => {
      post.mockResolvedValue({ data: { success: false } });
      await expect(startIntervention(payload)).rejects.toThrow("Unable to start intervention");
    });
  });
});
