import push, { buildArchive } from "./push";
import provideConfig from "./provideConfig";
import { fileFromPath } from "formdata-node/file-from-path";
import archiver from "archiver";
import * as fs from "node:fs/promises";
import * as fsSync from "node:fs";
import { findFiles } from "@embeddable.com/sdk-utils";
import { ResolvedEmbeddableConfig } from "./defineConfig";
import { vi, describe, it, expect, beforeEach } from "vitest";
import { getArgumentByKey } from "./utils";

// @ts-ignore
import reportErrorToRollbar from "./rollbar.mjs";
import { checkBuildSuccess, checkNodeVersion } from "./utils";
import { server } from "../../../mocks/server";
import { http, HttpResponse } from "msw";

const infoMock = {
  info: vi.fn(),
  succeed: vi.fn(),
  fail: vi.fn(),
};

const startMock = {
  succeed: vi.fn(),
  info: () => infoMock,
  fail: vi.fn(),
};

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

vi.mock("./utils", () => ({
  checkNodeVersion: vi.fn(),
  checkBuildSuccess: vi.fn(),
  getArgumentByKey: vi.fn(),
  getSDKVersions: vi.fn(),
}));

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

vi.mock("node:fs", () => ({
  createWriteStream: vi.fn(),
}));

vi.mock("./provideConfig", () => ({
  default: vi.fn().mockResolvedValue({
    client: {
      rootDir: "rootDir",
      buildDir: "buildDir",
      archiveFile: "embeddable-build.zip",
    },
  }),
}));

vi.mock("./rollbar.mjs", () => ({
  default: vi.fn(),
}));

vi.mock("@embeddable.com/sdk-utils", () => ({
  findFiles: vi.fn(),
}));

vi.mock("archiver", () => ({
  default: {
    create: vi.fn(),
  },
}));

vi.mock("formdata-node/file-from-path", () => ({
  fileFromPath: vi.fn().mockReturnValue(new Blob([new ArrayBuffer(8)])),
}));

const config = {
  client: {
    rootDir: "rootDir",
    buildDir: "buildDir",
    archiveFile: "embeddable-build.zip",
    globalCss: "src/global.css",
  },
  pushBaseUrl: "http://localhost:3000",
  previewBaseUrl: "http://localhost:3000",
  pushComponents: true,
};

describe("push", () => {
  const archiveMock = {
    finalize: vi.fn(),
    pipe: vi.fn(),
    directory: vi.fn(),
    file: vi.fn(),
  };

  beforeEach(() => {
    vi.mocked(checkNodeVersion).mockResolvedValue(true);
    vi.mocked(checkBuildSuccess).mockResolvedValue(true);
    vi.mocked(getArgumentByKey).mockReturnValue(undefined);
    vi.mocked(provideConfig).mockResolvedValue(
      config as ResolvedEmbeddableConfig,
    );
    vi.mocked(fs.access).mockResolvedValue(undefined);
    vi.mocked(fs.readFile).mockImplementation(async () =>
      Buffer.from(`{"access_token":"mocked-token"}`),
    );
    vi.mocked(fs.stat).mockResolvedValue({
      size: 100,
    } as any);
    vi.mocked(findFiles).mockResolvedValue([["fileName", "filePath"]]);
    vi.mocked(fileFromPath).mockReturnValue(
      new Blob([new ArrayBuffer(8)]) as any,
    );
    vi.mocked(archiver.create).mockReturnValue(archiveMock as any);

    vi.mocked(fsSync.createWriteStream).mockReturnValue({
      on: (event: string, cb: () => void) => {
        cb();
      },
      end: vi.fn(),
    } as any);

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

  it("should push the build", async () => {
    await push();

    expect(provideConfig).toHaveBeenCalled();
    expect(checkNodeVersion).toHaveBeenCalled();
    expect(checkBuildSuccess).toHaveBeenCalled();

    expect(fs.access).toHaveBeenCalledWith(config.client.buildDir);

    expect(archiver.create).toHaveBeenCalledWith("zip", {
      zlib: { level: 9 },
    });
    expect(fsSync.createWriteStream).toHaveBeenCalledWith(
      config.client.archiveFile,
    );

    expect(archiveMock.pipe).toHaveBeenCalled();
    expect(archiveMock.file).toHaveBeenCalledWith("src/global.css", {
      name: "global.css",
    });
    expect(archiveMock.directory).toHaveBeenCalledWith("buildDir", false);
    expect(archiveMock.finalize).toHaveBeenCalled();
    // after publishing the file gets removed
    expect(fs.rm).toHaveBeenCalledWith(config.client.archiveFile);

    expect(infoMock.info).toHaveBeenCalledWith(
      "Publishing to mocked-workspace-name using http://localhost:3000/workspace/mocked-workspace-id...",
    );
    expect(infoMock.succeed).toHaveBeenCalledWith(
      "Published to mocked-workspace-name using http://localhost:3000/workspace/mocked-workspace-id",
    );
  });

  it("should fail if there are no workspaces", async () => {
    vi.spyOn(console, "error").mockImplementation(() => undefined);
    server.use(
      http.get("**/workspace", () => {
        return HttpResponse.json([]);
      }),
    );

    await push();

    expect(startMock.fail).toHaveBeenCalledWith("No workspaces found");
    expect(process.exit).toHaveBeenCalledWith(1);
    expect(reportErrorToRollbar).toHaveBeenCalled();
  });

  it("should fail if the build is not successful", async () => {
    vi.spyOn(console, "error").mockImplementation(() => undefined);
    vi.mocked(checkBuildSuccess).mockResolvedValue(false);

    await push();

    expect(process.exit).toHaveBeenCalledWith(1);

    expect(console.error).toHaveBeenCalledWith(
      "Build failed or not completed. Please run `embeddable:build` first.",
    );
  });

  it("should push by api key provided in the arguments", async () => {
    vi.mocked(getArgumentByKey).mockReturnValue("mocked-api-key");
    Object.defineProperties(process, {
      argv: {
        value: [
          "--api-key",
          "mocked-api-key",
          "--email",
          "mocked-email@valid.com",
        ],
      },
    });

    await push();

    expect(startMock.succeed).toHaveBeenCalledWith("Published using API key");
  });

  describe("push configuration", () => {
    it("should fail if both pushModels and pushComponents are disabled", async () => {
      vi.spyOn(console, "error").mockImplementation(() => undefined);
      vi.mocked(provideConfig).mockResolvedValue({
        ...config,
        pushModels: false,
        pushComponents: false,
      } as ResolvedEmbeddableConfig);

      await push();

      expect(startMock.fail).toHaveBeenCalledWith(
        "Cannot push: both pushModels and pushComponents are disabled",
      );
      expect(process.exit).toHaveBeenCalledWith(1);
    });

    it("should only include component files when pushModels is false", async () => {
      const mockArchiver = {
        finalize: vi.fn(),
        pipe: vi.fn(),
        directory: vi.fn(),
        file: vi.fn(),
      };
      vi.mocked(archiver.create).mockReturnValue(mockArchiver as any);

      vi.mocked(provideConfig).mockResolvedValue({
        ...config,
        pushModels: false,
        pushComponents: true,
      } as ResolvedEmbeddableConfig);

      await push();

      // Should include component build directory
      expect(mockArchiver.directory).toHaveBeenCalled();
      // Should not include model files (except global.css which is part of components)
      expect(mockArchiver.file).toHaveBeenCalledTimes(2);
      expect(mockArchiver.file).toHaveBeenCalledWith(expect.anything(), {
        name: "global.css",
      });
    });
  });

  describe("API key validation", () => {
    it("should fail if API key is not provided", async () => {
      vi.spyOn(console, "error").mockImplementation(() => undefined);
      Object.defineProperties(process, {
        argv: {
          value: ["--api-key"],
        },
      });
      vi.mocked(getArgumentByKey).mockReturnValue(undefined);

      await push();

      expect(startMock.fail).toHaveBeenCalledWith("No API key provided");
      expect(process.exit).toHaveBeenCalledWith(1);
    });

    it("should fail if email is not provided with API key", async () => {
      vi.spyOn(console, "error").mockImplementation(() => undefined);
      Object.defineProperties(process, {
        argv: {
          value: ["--api-key", "some-key"],
        },
      });
      vi.mocked(getArgumentByKey)
        .mockReturnValueOnce("some-key") // API key
        .mockReturnValueOnce(undefined); // Email

      await push();

      expect(startMock.fail).toHaveBeenCalledWith(
        "Invalid email provided. Please provide a valid email using --email (-e) flag",
      );
      expect(process.exit).toHaveBeenCalledWith(1);
    });

    it("should fail if email is invalid", async () => {
      vi.spyOn(console, "error").mockImplementation(() => undefined);
      Object.defineProperties(process, {
        argv: {
          value: ["--api-key", "some-key", "--email", "invalid-email"],
        },
      });
      vi.mocked(getArgumentByKey)
        .mockReturnValueOnce("some-key") // API key
        .mockReturnValueOnce("invalid-email"); // Invalid email

      await push();

      expect(startMock.fail).toHaveBeenCalledWith(
        "Invalid email provided. Please provide a valid email using --email (-e) flag",
      );
      expect(process.exit).toHaveBeenCalledWith(1);
    });

    it("should accept optional message parameter", async () => {
      Object.defineProperties(process, {
        argv: {
          value: [
            "--api-key",
            "some-key",
            "--email",
            "valid@email.com",
            "--message",
            "test message",
          ],
        },
      });
      vi.mocked(getArgumentByKey)
        .mockReturnValueOnce("some-key") // API key
        .mockReturnValueOnce("valid@email.com") // Email
        .mockReturnValueOnce("test message"); // Message

      await push();

      expect(startMock.succeed).toHaveBeenCalledWith("Published using API key");
    });
  });

  describe("error handling", () => {
    beforeEach(() => {
      // Reset all mocks to their default state
      vi.mocked(getArgumentByKey).mockReturnValue(undefined);
      Object.defineProperties(process, {
        argv: {
          value: [],
        },
      });
    });

    it("should fail if build directory does not exist", async () => {
      vi.spyOn(console, "error").mockImplementation(() => undefined);
      vi.mocked(fs.access).mockRejectedValue(new Error("No such directory"));
      vi.mocked(provideConfig).mockResolvedValue(
        config as ResolvedEmbeddableConfig,
      );

      await push();

      expect(console.error).toHaveBeenCalledWith(
        "No embeddable build was produced.",
      );
      expect(process.exit).toHaveBeenCalledWith(1);
    });

    it("should fail if token is not available", async () => {
      vi.spyOn(console, "error").mockImplementation(() => undefined);
      vi.mocked(fs.access).mockResolvedValue(undefined);
      vi.mocked(fs.readFile).mockResolvedValue(Buffer.from("{}"));
      vi.mocked(provideConfig).mockResolvedValue(
        config as ResolvedEmbeddableConfig,
      );

      await push();

      expect(console.error).toHaveBeenCalledWith(
        "Expired token. Please login again.",
      );
      expect(process.exit).toHaveBeenCalledWith(1);
    });

    it("should handle and report errors during push", async () => {
      const testError = new Error("Test error");
      vi.mocked(provideConfig).mockRejectedValue(testError);
      vi.mocked(fs.access).mockResolvedValue(undefined);
      vi.mocked(fs.readFile).mockImplementation(async () =>
        Buffer.from(`{"access_token":"mocked-token"}`),
      );

      await push();

      expect(reportErrorToRollbar).toHaveBeenCalledWith(testError);
      expect(process.exit).toHaveBeenCalledWith(1);
    });
  });

  describe("buildArchive", () => {
    type MockArchiver = {
      finalize: ReturnType<typeof vi.fn>;
      pipe: ReturnType<typeof vi.fn>;
      directory: ReturnType<typeof vi.fn>;
      file: ReturnType<typeof vi.fn>;
    };

    let mockArchiver: MockArchiver;
    let mockOra: {
      start: ReturnType<typeof vi.fn>;
      succeed: ReturnType<typeof vi.fn>;
      fail: ReturnType<typeof vi.fn>;
    };

    beforeEach(() => {
      mockArchiver = {
        finalize: vi.fn(),
        pipe: vi.fn(),
        directory: vi.fn(),
        file: vi.fn(),
      };

      vi.mocked(archiver.create).mockReturnValue(mockArchiver as any);
      vi.mocked(findFiles).mockResolvedValue([]);
    });

    it("should include all file types when both flags are true", async () => {
      vi.mocked(findFiles)
        .mockResolvedValueOnce([
          ["model1.cube.yml", "/path/to/model1.cube.yml"],
          ["model2.cube.yaml", "/path/to/model2.cube.yaml"],
        ])
        .mockResolvedValueOnce([
          ["context1.sc.yml", "/path/to/context1.sc.yml"],
          ["context2.cc.yml", "/path/to/context2.cc.yml"],
        ]);

      const testConfig = {
        ...config,
        pushModels: true,
        pushComponents: true,
        client: {
          ...config.client,
          srcDir: "/src",
        },
      } as ResolvedEmbeddableConfig;

      await buildArchive(testConfig);

      // Should include component build directory
      expect(mockArchiver.directory).toHaveBeenCalledWith(
        testConfig.client.buildDir,
        false,
      );
      // Should include global.css
      expect(mockArchiver.file).toHaveBeenCalledWith(
        testConfig.client.globalCss,
        {
          name: "global.css",
        },
      );
      // Should include all model files
      expect(mockArchiver.file).toHaveBeenCalledWith(
        "/path/to/model1.cube.yml",
        {
          name: "model1.cube.yml",
        },
      );
      expect(mockArchiver.file).toHaveBeenCalledWith(
        "/path/to/model2.cube.yaml",
        {
          name: "model2.cube.yaml",
        },
      );
      // Should include all preset files
      expect(mockArchiver.file).toHaveBeenCalledWith(
        "/path/to/context1.sc.yml",
        {
          name: "context1.sc.yml",
        },
      );
      expect(mockArchiver.file).toHaveBeenCalledWith(
        "/path/to/context2.cc.yml",
        {
          name: "context2.cc.yml",
        },
      );
    });

    it("should only include component files when pushModels is false", async () => {
      const testConfig = {
        ...config,
        pushModels: false,
        pushComponents: true,
        client: {
          ...config.client,
          srcDir: "/src",
        },
      } as ResolvedEmbeddableConfig;

      await buildArchive(testConfig);

      // Should include component build directory
      expect(mockArchiver.directory).toHaveBeenCalledWith(
        testConfig.client.buildDir,
        false,
      );
      // Should include global.css
      expect(mockArchiver.file).toHaveBeenCalledWith(
        testConfig.client.globalCss,
        {
          name: "global.css",
        },
      );
      // Should only find client context files
      expect(findFiles).toHaveBeenCalledOnce();
    });

    it("should search in custom directories for model files", async () => {
      const testConfig = {
        ...config,
        pushModels: true,
        pushComponents: true,
        client: {
          ...config.client,
          srcDir: "/src",
          modelsSrc: "/custom/models/path",
          presetsSrc: "/custom/presets/path",
        },
      } as ResolvedEmbeddableConfig;

      await buildArchive(testConfig);

      expect(findFiles).toHaveBeenCalledWith(
        "/custom/models/path",
        expect.any(RegExp),
      );
      expect(findFiles).toHaveBeenCalledWith(
        "/custom/presets/path",
        expect.any(RegExp),
      );
    });

    it("should use srcDir as fallback when modelsSrc/presetsSrc are not defined", async () => {
      const testConfig = {
        ...config,
        pushModels: true,
        pushComponents: true,
        client: {
          ...config.client,
          srcDir: "/src",
          modelsSrc: undefined,
          presetsSrc: undefined,
        },
      } as unknown as ResolvedEmbeddableConfig;

      await buildArchive(testConfig);

      expect(findFiles).toHaveBeenCalledWith("/src", expect.any(RegExp));
      expect(findFiles).toHaveBeenCalledWith("/src", expect.any(RegExp));
    });
  });
});
