import type { IDockerComposeResult } from "docker-compose";
import { MockAgent, setGlobalDispatcher } from "undici";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

// Mock docker-compose before importing the module under test
const versionMock =
  vi.fn<() => Promise<IDockerComposeResult & { data: { version: string } }>>();

vi.doMock("docker-compose", () => ({
  version: versionMock,
}));

// Create manual installer adapter mock
const manualInstallerAdapterMock = {
  install: vi.fn<(version: string) => Promise<void>>(),
};

// Dynamic import after mock setup
const { DockerComposeInstallerService } = await import(
  "./docker-compose-installer.service.js"
);

describe("DockerComposeInstallerService", () => {
  let mockAgent: MockAgent;
  let service: InstanceType<typeof DockerComposeInstallerService>;

  const composeVersionResponse = (version: string) => ({
    exitCode: 0,
    out: "",
    err: "",
    data: {
      version,
    },
  });

  const installCompose = (
    composeVersion: string | null,
    githubToken: string | null,
  ) =>
    service.install({
      composeVersion,
      cwd: "/path/to/cwd",
      githubToken,
    });

  const setPlatform = (platform: NodeJS.Platform) => {
    Object.defineProperty(process, "platform", {
      value: platform,
    });
  };

  const mockLatestRelease = (version: string) => {
    const mockClient = mockAgent.get("https://api.github.com");
    mockClient
      .intercept({
        path: "/repos/docker/compose/releases/latest",
        method: "GET",
      })
      .reply(
        200,
        {
          tag_name: version,
        },
        {
          headers: {
            "content-type": "application/json",
          },
        },
      );
    setGlobalDispatcher(mockClient);
    Object.defineProperty(globalThis, "fetch", {
      value: vi.fn(),
    });
  };

  beforeEach(() => {
    vi.clearAllMocks();
    mockAgent = new MockAgent();
    mockAgent.disableNetConnect();

    service = new DockerComposeInstallerService(
      manualInstallerAdapterMock as never,
    );
  });

  afterEach(() => {
    vi.resetAllMocks();
  });

  describe("install", () => {
    it("should install latest when compose version is not specified and Compose is missing", async () => {
      // Arrange: first call to version() fails (Compose missing)
      versionMock.mockRejectedValueOnce(new Error("version not installed"));

      const latestVersion = "v2.0.0";
      mockLatestRelease(latestVersion);
      versionMock.mockResolvedValueOnce(composeVersionResponse(latestVersion));
      setPlatform("linux");

      // Act
      const result = await installCompose(null, "token");

      // Assert
      expect(result).toBe(latestVersion);
      expect(manualInstallerAdapterMock.install).toHaveBeenCalledWith(
        latestVersion,
      );
    });

    it("should return current version when no version is provided", async () => {
      // Arrange
      versionMock.mockResolvedValue(composeVersionResponse("2.0.0"));

      // Act
      const result = await installCompose(null, null);

      // Assert
      expect(result).toBe("2.0.0");
      expect(manualInstallerAdapterMock.install).not.toHaveBeenCalled();
    });

    it("should not install anything when expected version is already installed", async () => {
      // Arrange
      versionMock.mockResolvedValue(composeVersionResponse("1.2.3"));

      // Act
      const result = await installCompose("v1.2.3", null);

      // Assert
      expect(result).toBe("1.2.3");
      expect(manualInstallerAdapterMock.install).not.toHaveBeenCalled();
    });

    it("should install the requested version if it is not already installed", async () => {
      // Arrange
      versionMock.mockResolvedValueOnce(composeVersionResponse("1.2.3"));

      const expectedVersion = "1.3.0";
      versionMock.mockResolvedValueOnce(
        composeVersionResponse(expectedVersion),
      );
      setPlatform("linux");

      // Act
      const result = await installCompose(expectedVersion, null);

      // Assert
      expect(result).toBe(expectedVersion);
      expect(manualInstallerAdapterMock.install).toHaveBeenCalledWith(
        expectedVersion,
      );
    });

    it("should install the latest version if requested", async () => {
      // Arrange
      versionMock.mockResolvedValueOnce(composeVersionResponse("1.2.3"));

      const latestVersion = "v1.4.0";
      mockLatestRelease(latestVersion);
      versionMock.mockResolvedValueOnce(composeVersionResponse(latestVersion));
      setPlatform("linux");

      // Act
      const result = await installCompose("latest", "token");

      // Assert
      expect(result).toBe(latestVersion);
      expect(manualInstallerAdapterMock.install).toHaveBeenCalledWith(
        latestVersion,
      );
    });

    it("should throw an error if the latest version if requested and no Github token is provided", async () => {
      // Arrange
      versionMock.mockResolvedValueOnce(composeVersionResponse("1.2.3"));

      // Act & Assert
      await expect(installCompose("latest", null)).rejects.toThrow(
        "GitHub token is required to install the latest version",
      );
    });

    it("should throw an error on unsupported platforms", async () => {
      // Arrange
      versionMock.mockResolvedValueOnce(composeVersionResponse("1.2.3"));

      const expectedVersion = "1.3.0";
      versionMock.mockResolvedValueOnce(
        composeVersionResponse(expectedVersion),
      );
      setPlatform("win32");

      // Act & Assert
      await expect(installCompose(expectedVersion, null)).rejects.toThrow(
        `Unsupported platform: win32`,
      );

      expect(manualInstallerAdapterMock.install).not.toHaveBeenCalled();
    });

    it("should install when version check fails", async () => {
      // Arrange: first call to version() doesn't find
      versionMock.mockRejectedValueOnce(new Error("version not installed"));

      const installedVersion = "2.0.0";

      // After installation, version() returns the new version
      versionMock.mockResolvedValueOnce(
        composeVersionResponse(installedVersion),
      );
      setPlatform("linux");

      // Act
      const result = await installCompose(installedVersion, "token");

      // Assert
      expect(result).toBe(installedVersion);
      expect(manualInstallerAdapterMock.install).toHaveBeenCalledWith(
        installedVersion,
      );
    });

    it("should install latest version when missing or unspecified", async () => {
      // Arrange: first call to version() doesn't find
      versionMock.mockRejectedValueOnce(new Error("version check failed"));
      // second call finds newly installed version
      versionMock.mockResolvedValueOnce(composeVersionResponse("v1.4.0"));

      const latestVersion = "v1.4.0";
      mockLatestRelease(latestVersion);
      versionMock.mockResolvedValueOnce(composeVersionResponse(latestVersion));
      setPlatform("linux");

      // Act
      const result = await installCompose("latest", "token");

      // Assert
      expect(result).toBe(latestVersion);
      expect(manualInstallerAdapterMock.install).toHaveBeenCalledWith(
        latestVersion,
      );
    });

    it("should throw if Compose is missing and no GitHub token is provided", async () => {
      // Arrange: first call to version() doesn't find
      versionMock.mockRejectedValueOnce(new Error("version check failed"));
      setPlatform("linux");

      await expect(installCompose("latest", null)).rejects.toThrow(
        "GitHub token is required to install the latest version",
      );
    });

    it("should not install when the version is already installed and no version is specified", async () => {
      // Arrange
      versionMock.mockResolvedValue(composeVersionResponse("1.2.3"));

      // Act
      const result = await installCompose("", null);

      // Assert
      expect(result).toBe("1.2.3");
      expect(manualInstallerAdapterMock.install).not.toHaveBeenCalled();
    });

    it("should throw when installed version does not match target", async () => {
      // Arrange
      versionMock.mockResolvedValueOnce(composeVersionResponse("1.2.3"));

      const targetVersion = "v1.4.0";
      versionMock.mockResolvedValueOnce(composeVersionResponse("1.3.0"));
      setPlatform("linux");

      // Act & Assert
      await expect(installCompose(targetVersion, "token")).rejects.toThrow(
        `Failed to install Docker Compose version "${targetVersion}", installed version is "1.3.0"`,
      );
      expect(manualInstallerAdapterMock.install).toHaveBeenCalledWith(
        targetVersion,
      );
    });

    it("should throw with unknown installed version when post-install version check fails", async () => {
      // Arrange
      versionMock.mockResolvedValueOnce(composeVersionResponse("1.2.3"));

      const targetVersion = "v1.4.0";
      versionMock.mockRejectedValueOnce(
        new Error("version check failed after install"),
      );
      setPlatform("linux");

      // Act & Assert
      await expect(installCompose(targetVersion, "token")).rejects.toThrow(
        `Failed to install Docker Compose version "${targetVersion}", installed version is "unknown"`,
      );
      expect(manualInstallerAdapterMock.install).toHaveBeenCalledWith(
        targetVersion,
      );
    });
  });
});
