import { beforeEach, describe, expect, it, vi } from "vitest";
import { api } from "../api/apiClient";
import { CryptoUtils } from "./crypto";
import { blobUrlToFile } from "./fileUtils";
import {
  decryptCanvasImages,
  decryptCanvasImagesWithSharingKey,
  encryptCanvasImages,
} from "./letterLogic";

vi.mock("../api/apiClient", () => ({
  api: {
    get: vi.fn(),
  },
  apiServerUrl: "https://remote",
}));

vi.mock("./fileUtils", () => ({
  blobUrlToFile: vi.fn(),
}));

describe("letterLogic image helpers", () => {
  let masterKey: CryptoKey;
  let crypto: CryptoUtils;
  beforeEach(async () => {
    const keyBundle = await CryptoUtils.deriveKeyBundle(
      "password123",
      "test@example.com",
    );
    masterKey = keyBundle.masterKey;
    crypto = new CryptoUtils();
    await crypto.initialize();
    vi.clearAllMocks();
  });

  describe("encryptCanvasImages", () => {
    it("should not encrypt images whose src already ends with .bin", async () => {
      const canvasData = {
        objects: [
          {
            type: "Image",
            src: "already-encrypted.png.bin",
            top: 0,
            left: 0,
            width: 100,
            height: 100,
          },
          {
            type: "Textbox",
            text: "hello",
            top: 0,
            left: 0,
            width: 100,
            height: 100,
          },
        ],
      };

      const encryptImageSpy = vi.spyOn(CryptoUtils.prototype, "encryptImage");

      const { encryptedImageFiles: uploads, encryptedCanvasData } =
        await encryptCanvasImages(canvasData, [], masterKey, crypto);

      expect(encryptImageSpy).not.toHaveBeenCalled();
      expect(encryptedCanvasData.objects[0].src).toBe(
        "already-encrypted.png.bin",
      );
      expect(uploads.size).toBe(0);
    });

    it("should encrypt new blob-backed images and return encrypted uploads", async () => {
      const file = new File(["img"], "photo.png", { type: "image/png" });
      const canvasData = {
        objects: [
          {
            type: "Image",
            src: "blob:http://localhost/test-image",
            top: 0,
            left: 0,
            width: 100,
            height: 100,
          },
        ],
      };
      const canvasImages = [
        {
          src: "blob:http://localhost/test-image",
          file,
        },
      ];

      vi.spyOn(CryptoUtils.prototype, "encryptImage").mockResolvedValue({
        encryptedBlob: new Blob(["encrypted"], {
          type: "application/octet-stream",
        }),
        encrypted_dek: "wrapped-image-dek",
        filename: "photo.png.bin",
      });

      const { encryptedImageFiles: uploads, encryptedCanvasData } =
        await encryptCanvasImages(canvasData, canvasImages, masterKey, crypto);

      expect(CryptoUtils.prototype.encryptImage).toHaveBeenCalledTimes(1);
      expect(encryptedCanvasData.objects[0].src).toBe("photo.png.bin");
      expect(uploads.size).toBe(1);
      expect(uploads.has("photo.png.bin")).toBe(true);
    });
  });

  describe("decryptCanvasImages", () => {
    it("should decrypt images and replace src with blob URL", async () => {
      const canvasData = {
        objects: [
          {
            type: "Image",
            src: "photo.png.bin",
            top: 0,
            left: 0,
            width: 100,
            height: 100,
          },
          {
            type: "Textbox",
            text: "hello",
            top: 0,
            left: 0,
            width: 100,
            height: 100,
          },
        ],
      };
      const remoteImages = [
        { file_name: "photo.png.bin", file: `https://remote/photo.png.bin` },
      ];

      vi.mocked(api.get).mockResolvedValue({ data: new Blob(["encrypted"]) });
      vi.spyOn(CryptoUtils.prototype, "decryptImage").mockResolvedValue(
        "blob:http://localhost/decrypted",
      );

      const { canvasDataWithDecryptedImages } = await decryptCanvasImages(
        canvasData,
        remoteImages,
        "wrapped-dek",
        masterKey,
        crypto,
      );

      expect(api.get).toHaveBeenCalledWith(
        `https://remote/photo.png.bin`,
        expect.objectContaining({ responseType: "blob" }),
      );
      expect(CryptoUtils.prototype.decryptImage).toHaveBeenCalledWith(
        expect.any(Blob),
        "wrapped-dek",
        masterKey,
      );
      expect(canvasDataWithDecryptedImages.objects[0].src).toBe(
        "blob:http://localhost/decrypted",
      );
      expect(canvasDataWithDecryptedImages.objects[1].text).toBe("hello");
    });

    it("should include raw file when includeRawFile is true", async () => {
      const canvasData = {
        objects: [
          {
            type: "Image",
            src: "photo.png.bin",
            _customRawFile: null,
            top: 0,
            left: 0,
            width: 100,
            height: 100,
          },
        ],
      };
      const remoteImages = [
        { file_name: "photo.png.bin", file: "https://remote/photo.png.bin" },
      ];

      vi.mocked(api.get).mockResolvedValue({ data: new Blob(["encrypted"]) });
      vi.spyOn(CryptoUtils.prototype, "decryptImage").mockResolvedValue(
        "blob:http://localhost/decrypted",
      );
      vi.mocked(blobUrlToFile).mockResolvedValue(
        new File(["raw"], "photo.png.bin"),
      );

      const { canvasDataWithDecryptedImages } = await decryptCanvasImages(
        canvasData,
        remoteImages,
        "wrapped-dek",
        masterKey,
        crypto,
        true,
      );

      expect(blobUrlToFile).toHaveBeenCalledWith(
        "blob:http://localhost/decrypted",
        "photo.png.bin",
      );
      expect(
        canvasDataWithDecryptedImages.objects[0]._customRawFile,
      ).toBeInstanceOf(File);
    });
  });

  describe("decryptCanvasImagesWithSharingKey", () => {
    it("should decrypt images using sharing key", async () => {
      const canvasData = {
        objects: [
          {
            type: "Image",
            src: "photo.png.bin",
            top: 0,
            left: 0,
            width: 100,
            height: 100,
          },
        ],
      };
      const remoteImages = [
        {
          public_id: "1234",
          file_name: "photo.png.bin",
          file: "https://remote/photo.png.bin",
        },
      ];

      vi.mocked(api.get).mockResolvedValue({ data: new Blob(["encrypted"]) });
      vi.spyOn(
        CryptoUtils.prototype,
        "decryptImageWithSharingKey",
      ).mockResolvedValue("blob:http://localhost/decrypted-shared");

      const { canvasDataWithDecryptedImages } =
        await decryptCanvasImagesWithSharingKey(
          canvasData,
          remoteImages,
          "raw-sharing-key",
          crypto,
        );

      expect(api.get).toHaveBeenCalledWith(
        "https://remote/photo.png.bin",
        expect.objectContaining({ responseType: "blob" }),
      );
      expect(
        CryptoUtils.prototype.decryptImageWithSharingKey,
      ).toHaveBeenCalledWith(expect.any(Blob), "raw-sharing-key");
      expect(canvasDataWithDecryptedImages.objects[0].src).toBe(
        "blob:http://localhost/decrypted-shared",
      );
    });
  });
});
