import * as fs from "node:fs/promises";
import {
  checkNodeVersion,
  getArgumentByKey,
  storeBuildSuccessFlag,
  checkBuildSuccess,
  SUCCESS_FLAG_FILE,
  hrtimeToISO8601,
  getSDKVersions,
} from "./utils";

const startMock = {
  succeed: vi.fn(),
  fail: vi.fn(),
  stop: vi.fn(),
};

const failMock = vi.fn();

// Mock for testing resolveLocalFileVersion
const mockPackageJson = {
  version: "1.2.3",
};

vi.mock("ora", () => ({
  default: () => ({
    start: vi.fn().mockImplementation(() => startMock),
    info: vi.fn(),
    fail: failMock,
  }),
}));

vi.mock("../package.json", () => ({
  engines: {
    node: "14.x",
  },
}));

vi.mock("fs/promises", () => ({
  readFile: vi.fn(),
  writeFile: vi.fn(),
  access: vi.fn(),
  mkdir: vi.fn(),
  unlink: vi.fn(),
}));

describe("utils", () => {
  describe("checkNodeVersion", () => {
    it("should return true if the node version is greater than 14", async () => {
      vi.mocked(fs.readFile).mockResolvedValue(
        JSON.stringify({
          engines: {
            node: "16.x",
          },
        }),
      );
      const result = await checkNodeVersion();

      expect(result).toBe(true);
    });

    it("should fail if the node version is less than 16", async () => {
      Object.defineProperty(process.versions, "node", {
        value: "14.0",
        configurable: true,
      });

      vi.mocked(fs.readFile).mockResolvedValue(
        JSON.stringify({
          engines: {
            node: "16.x",
          },
        }),
      );

      vi.spyOn(process, "exit").mockImplementation(() => null as never);

      const result = await checkNodeVersion();
      expect(startMock.fail).toHaveBeenCalledWith(
        "Node version 16.0 or higher is required. You are running 14.0.",
      );

      expect(result).toBe(undefined);
    });

    it("should throw an error if the package.json file cannot be read", async () => {
      vi.spyOn(process, "exit").mockImplementation(() => null as never);
      vi.mocked(fs.readFile).mockRejectedValue(
        new Error("Failed to read package.json of core-sdk"),
      );
      await expect(checkNodeVersion()).rejects.toThrow(
        "Failed to read package.json of core-sdk",
      );
    });
  });

  describe("getArgumentByKey", () => {
    it("should return the value of the argument", () => {
      process.argv = ["--email", "test@a.com", "--password", "123456"];
      const result = getArgumentByKey("--email");

      expect(result).toBe("test@a.com");

      process.argv = ["-e", "test@a.com", "--password", "123456"];
      const result2 = getArgumentByKey(["--email", "-e"]);

      expect(result2).toBe("test@a.com");
    });
  });

  describe("storeBuildSuccessFlag", () => {
    it("should write a success flag file", async () => {
      await storeBuildSuccessFlag();

      expect(fs.writeFile).toHaveBeenCalledWith(SUCCESS_FLAG_FILE, "true");
    });

    it("should throw an error if the file write fails", async () => {
      vi.mocked(fs.writeFile).mockImplementation(async () => {
        throw new Error();
      });

      await expect(storeBuildSuccessFlag()).rejects.toThrow();
    });
  });

  describe("checkBuildSuccess", () => {
    it("should return true if the success flag file exists", async () => {
      vi.mocked(fs.access).mockResolvedValue(undefined);

      const result = await checkBuildSuccess();

      expect(result).toBe(true);
    });

    it("should return false if the success flag file does not exist", async () => {
      vi.mocked(fs.access).mockRejectedValue(undefined);

      const result = await checkBuildSuccess();

      expect(result).toBe(false);
    });
  });

  describe("getSDKVersions", () => {
    beforeEach(() => {
      process.env.NODE_ENV = "local";
      vi.resetAllMocks();
      process.cwd = vi.fn().mockReturnValue("/test/path");
    });

    afterEach(() => {
      process.env.NODE_ENV = "test";
    });

    it("should get versions from node_modules", async () => {
      // Mock the readFile to return a version for the first package
      vi.mocked(fs.readFile).mockResolvedValueOnce(
        JSON.stringify(mockPackageJson),
      );

      const result = await getSDKVersions();

      expect(result).toHaveProperty("@embeddable.com/core", "1.2.3");
    });

    it("should handle local file references", async () => {
      // Mock package.json with local file references
      const mockProjectPackageJson = {
        dependencies: {
          "@embeddable.com/core": "file:../packages/core",
        },
      };

      // First readFile calls for node_modules fail
      vi.mocked(fs.readFile)
        .mockRejectedValueOnce(new Error("Not found")) // core
        .mockRejectedValueOnce(new Error("Not found")) // react
        .mockRejectedValueOnce(new Error("Not found")) // sdk-core
        .mockRejectedValueOnce(new Error("Not found")) // sdk-react
        .mockRejectedValueOnce(new Error("Not found")) // sdk-utils
        // Then project package.json succeeds
        .mockResolvedValueOnce(JSON.stringify(mockProjectPackageJson))
        // Then the referenced package.json succeeds
        .mockResolvedValueOnce(JSON.stringify({ version: "2.0.0" }));

      const result = await getSDKVersions();

      expect(result).toHaveProperty("@embeddable.com/core", "2.0.0");
    });

    it("should handle errors when resolving local file references", async () => {
      // Mock console.warn to avoid test output noise
      const consoleWarnSpy = vi
        .spyOn(console, "warn")
        .mockImplementation(() => {});

      // Mock package.json with local file references
      const mockProjectPackageJson = {
        dependencies: {
          "@embeddable.com/core": "file:../packages/core",
        },
      };

      // First readFile calls for node_modules fail
      vi.mocked(fs.readFile)
        .mockRejectedValueOnce(new Error("Not found")) // core
        .mockRejectedValueOnce(new Error("Not found")) // react
        .mockRejectedValueOnce(new Error("Not found")) // sdk-core
        .mockRejectedValueOnce(new Error("Not found")) // sdk-react
        .mockRejectedValueOnce(new Error("Not found")) // sdk-utils
        // Then project package.json succeeds
        .mockResolvedValueOnce(JSON.stringify(mockProjectPackageJson))
        // Then the referenced package.json fails
        .mockRejectedValueOnce(new Error("Cannot read file"));

      const result = await getSDKVersions();

      expect(consoleWarnSpy).toHaveBeenCalled();
      expect(result).toHaveProperty("@embeddable.com/core", "local-dev");

      consoleWarnSpy.mockRestore();
    });

    it("should use fallback values in test environment when no versions found", async () => {
      // Mock console.warn to avoid test output noise
      const consoleWarnSpy = vi
        .spyOn(console, "warn")
        .mockImplementation(() => {});

      // Mock all readFile calls to fail
      vi.mocked(fs.readFile).mockRejectedValue(new Error("Not found"));

      // Mock process.cwd to return a test-sdk path
      process.cwd = vi.fn().mockReturnValue("/test/path/test-sdk");

      const result = await getSDKVersions();

      // Should have fallback values for all packages
      expect(Object.keys(result).length).toBeGreaterThan(0);
      expect(result["@embeddable.com/core"]).toBe("local-dev");
      expect(result["@embeddable.com/sdk-core"]).toBe("local-dev");

      consoleWarnSpy.mockRestore();
    });
  });

  describe("hrtimeToISO8601", () => {
    test.each([
      { input: [0, 0], expected: "PT0.000S" },
      { input: [75, 0], expected: "PT1M15.000S" }, // 1 minute and 15 seconds
      { input: [2, 500000000], expected: "PT2.500S" }, // 2.5 seconds
      { input: [0, 123456789], expected: "PT0.123S" }, // 0.123 seconds
      { input: [0, 500000000], expected: "PT0.500S" }, // 0.5 seconds
      { input: [1, 0], expected: "PT1.000S" }, // 1 second
      { input: null, expected: "" },
      { input: undefined, expected: "" },
    ])("converts hrtime $input to $expected", ({ input, expected }) => {
      expect(hrtimeToISO8601(input)).toBe(expected);
    });
  });
});
