jest.mock("../../src/services/apiClient", () => {
  class ApiClientError extends Error {
    status?: number;
    constructor(message: string, status?: number) {
      super(message);
      this.name = "ApiClientError";
      this.status = status;
    }
  }
  return {
    __esModule: true,
    default: { post: jest.fn() },
    ApiClientError,
  };
});

import apiClient, { ApiClientError } from "../../src/services/apiClient";
import {
  clearCrashLogs,
  flushCrashReports,
  readCrashLogs,
  recordCrash,
  sendTestCrashReport,
  setCrashContext,
  TEST_CRASH_MARKER,
} from "../../src/core/crashLog";

const mockPost = apiClient.post as jest.Mock;

describe("crashLog", () => {
  beforeEach(async () => {
    await clearCrashLogs();
    setCrashContext({ instanceUrl: null, tenantId: null });
    jest.clearAllMocks();
  });

  describe("persistance locale", () => {
    it("record puis read renvoie l'incident", async () => {
      await recordCrash({ kind: "fatal", message: "boom", stack: "at x", isFatal: true });
      const entries = await readCrashLogs();
      expect(entries).toHaveLength(1);
      expect(entries[0]).toMatchObject({ kind: "fatal", message: "boom", isFatal: true });
      expect(entries[0].timestamp).toBeDefined();
      expect(typeof entries[0].platform).toBe("string");
      expect(entries[0].app).toBeDefined();
    });

    it("joint le contexte instance/tenant courant (jamais de donnee sensible)", async () => {
      setCrashContext({ instanceUrl: "https://x/api/mobile", tenantId: 7 });
      await recordCrash({ kind: "render", message: "render fail" });
      const [entry] = await readCrashLogs();
      expect(entry.instanceUrl).toBe("https://x/api/mobile");
      expect(entry.tenantId).toBe(7);
    });

    it("borne l'anneau aux 50 derniers incidents", async () => {
      for (let i = 0; i < 55; i += 1) {
        await recordCrash({ kind: "fatal", message: `e${i}` });
      }
      const entries = await readCrashLogs();
      expect(entries).toHaveLength(50);
      expect(entries[0].message).toBe("e5");
      expect(entries[49].message).toBe("e54");
    });

    it("tronque les stacks tres longues", async () => {
      await recordCrash({ kind: "fatal", message: "x", stack: "a".repeat(5000) });
      const [entry] = await readCrashLogs();
      expect(entry.stack?.length).toBe(4000);
    });

    it("clear vide le journal", async () => {
      await recordCrash({ kind: "fatal", message: "x" });
      await clearCrashLogs();
      expect(await readCrashLogs()).toEqual([]);
    });
  });

  describe("flushCrashReports (push backend best-effort)", () => {
    it("ne fait aucun appel sans incident", async () => {
      const sent = await flushCrashReports();
      expect(sent).toBe(0);
      expect(mockPost).not.toHaveBeenCalled();
    });

    it("poste les incidents et vide le journal apres succes", async () => {
      mockPost.mockResolvedValue({ data: {}, status: 200 });
      await recordCrash({ kind: "fatal", message: "x" });
      await recordCrash({ kind: "rejection", message: "y" });

      const sent = await flushCrashReports();

      expect(sent).toBe(2);
      expect(mockPost).toHaveBeenCalledTimes(1);
      expect(mockPost).toHaveBeenCalledWith(
        "/mobile/crash_report.php",
        expect.objectContaining({ reports: expect.arrayContaining([]) })
      );
      const body = mockPost.mock.calls[0][1] as { reports: unknown[] };
      expect(body.reports).toHaveLength(2);
      expect(await readCrashLogs()).toEqual([]); // vide apres succes
    });

    it("bascule sur l'endpoint sans extension si le .php renvoie 404", async () => {
      mockPost
        .mockRejectedValueOnce(new ApiClientError("not found", 404))
        .mockResolvedValueOnce({ data: {}, status: 200 });
      await recordCrash({ kind: "fatal", message: "x" });

      const sent = await flushCrashReports();

      expect(sent).toBe(1);
      expect(mockPost).toHaveBeenCalledTimes(2);
      expect(mockPost).toHaveBeenLastCalledWith("/mobile/crash_report", expect.anything());
      expect(await readCrashLogs()).toEqual([]);
    });

    it("CONSERVE les logs si l'endpoint n'existe pas encore (404 partout)", async () => {
      mockPost.mockRejectedValue(new ApiClientError("not found", 404));
      await recordCrash({ kind: "fatal", message: "garde" });

      const sent = await flushCrashReports();

      expect(sent).toBe(0);
      expect(await readCrashLogs()).toHaveLength(1); // rien perdu
    });

    it("CONSERVE les logs sur erreur reseau / non authentifie", async () => {
      mockPost.mockRejectedValue(new ApiClientError("Unauthorized", 401));
      await recordCrash({ kind: "fatal", message: "garde" });

      const sent = await flushCrashReports();

      expect(sent).toBe(0);
      expect(await readCrashLogs()).toHaveLength(1);
    });
  });

  describe("sendTestCrashReport (diagnostic manuel)", () => {
    it("envoie un incident marque TEST puis vide le journal apres succes", async () => {
      mockPost.mockResolvedValue({ data: {}, status: 200 });

      const sent = await sendTestCrashReport();

      expect(sent).toBe(1);
      expect(mockPost).toHaveBeenCalledTimes(1);
      const body = mockPost.mock.calls[0][1] as { reports: Array<{ message: string }> };
      expect(body.reports).toHaveLength(1);
      expect(body.reports[0].message).toContain(TEST_CRASH_MARKER);
      expect(await readCrashLogs()).toEqual([]); // chemin de prod : vide apres 2xx
    });

    it("renvoie 0 et CONSERVE l'incident de test si le backend est injoignable", async () => {
      mockPost.mockRejectedValue(new ApiClientError("offline", 0));

      const sent = await sendTestCrashReport();

      expect(sent).toBe(0);
      const [entry] = await readCrashLogs();
      expect(entry.message).toContain(TEST_CRASH_MARKER); // reessaye au prochain sync
    });
  });
});
