import path from "node:path";
import { describe, it, expect, vi, beforeEach } from "vitest";
import * as fs from "node:fs/promises";
import * as fsSync from "node:fs";
import process from "node:process";

import buildPackage from "./buildPackage";
import buildTypes from "./buildTypes";
import buildGlobalHooks from "./buildGlobalHooks";
import provideConfig from "./provideConfig";
import {
  checkNodeVersion,
  removeBuildSuccessFlag,
  storeBuildSuccessFlag,
} from "./utils";
import { initLogger, logError } from "./logger";
import { ResolvedEmbeddableConfig } from "./defineConfig";

// ----------------- MOCKS ----------------- //
vi.mock("node:fs/promises");
vi.mock("node:fs");

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

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

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

// Mock utils
vi.mock("./utils", async () => {
  const actualUtils =
    await vi.importActual<typeof import("./utils")>("./utils");
  return {
    ...actualUtils,
    checkNodeVersion: vi.fn(),
    removeBuildSuccessFlag: vi.fn(),
    storeBuildSuccessFlag: vi.fn(),
  };
});

// Mock logger
vi.mock("./logger", () => ({
  initLogger: vi.fn().mockResolvedValue(undefined),
  logError: vi.fn().mockResolvedValue(undefined),
}));

// ----------------- TESTS ----------------- //
describe("buildPackage", () => {
  // We'll create a sample plugin for testing
  const mockPlugin = {
    pluginName: "testPlugin",
    validate: vi.fn(),
    buildPackage: vi.fn(),
  };

  const rootDir = path.resolve("fake", "root");
  const buildDir = path.resolve("fake", "build");
  const distDir = path.resolve(rootDir, "dist");

  // We'll also create a sample config
  const config = {
    client: {
      rootDir,
      buildDir,
    },
    plugins: [() => mockPlugin],
  } as unknown as ResolvedEmbeddableConfig;

  beforeEach(() => {
    // Clear mocks before each test to avoid cross-test interference
    vi.clearAllMocks();

    // By default, provideConfig will return our sample config
    vi.mocked(provideConfig).mockResolvedValue(config);

    // Let's ensure fsSync.existsSync returns false by default
    // (so that our prepare() logic tries to mkdir, etc.)
    vi.spyOn(fsSync, "existsSync").mockReturnValue(false);
  });

  it("should call all main steps in a successful scenario", async () => {
    await buildPackage();

    // 1. Logger
    expect(initLogger).toHaveBeenCalledWith("package");

    // 2. checkNodeVersion and removeBuildSuccessFlag
    expect(checkNodeVersion).toHaveBeenCalled();
    expect(removeBuildSuccessFlag).toHaveBeenCalled();

    // 3. provideConfig -> must be called, returns our config
    expect(provideConfig).toHaveBeenCalled();

    // 4. prepare() logic -> it checks if buildDir exists, removes if so, then mkdir
    // Because we do cross-platform checks, we confirm the path used is distDir
    expect(fsSync.existsSync).toHaveBeenCalledWith(distDir);
    // Because it returned false, we do not remove
    expect(fs.rm).toHaveBeenCalledTimes(0);
    // Instead we create it
    expect(fs.mkdir).toHaveBeenCalledWith(distDir);

    // 5. buildTypes & buildGlobalHooks
    expect(buildTypes).toHaveBeenCalledWith(config);
    expect(buildGlobalHooks).toHaveBeenCalledWith(config);

    // 6. Plugin calls
    expect(mockPlugin.validate).toHaveBeenCalledWith(config);
    expect(mockPlugin.buildPackage).toHaveBeenCalledWith(config);

    // 7. storeBuildSuccessFlag
    expect(storeBuildSuccessFlag).toHaveBeenCalled();
  });

  it("should remove and recreate buildDir if it already exists", async () => {
    // This time let's simulate that buildDir already exists
    vi.mocked(fsSync.existsSync).mockReturnValue(true);

    await buildPackage();

    // Because buildDir exists, prepare() should remove it
    expect(fs.rm).toHaveBeenCalledWith(distDir, { recursive: true });
    // Then re-create it
    expect(fs.mkdir).toHaveBeenCalledWith(distDir);
  });

  it("should log an error, call logError, and exit(1) if an error occurs", async () => {
    // We'll simulate an error in removeBuildSuccessFlag
    const error = new Error("test error");
    vi.mocked(removeBuildSuccessFlag).mockRejectedValueOnce(error);

    // We want to check process.exit(1) is called
    // but calling exit actually kills the process, so we mock it
    const originalConsoleLog = console.log;
    console.log = vi.fn();
    const exitSpy = vi.spyOn(process, "exit").mockImplementation(() => {
      throw new Error("process.exit called");
    });

    // We expect buildPackage to throw because exit is called
    await expect(buildPackage()).rejects.toThrow("process.exit called");

    // We also expect that logError has been called with the correct params
    expect(logError).toHaveBeenCalledWith({
      command: "package",
      breadcrumbs: ["checkNodeVersion"], // because it fails after calling checkNodeVersion
      error,
    });

    // And the error is printed
    expect(console.log).toHaveBeenCalledWith(error);

    // Finally, we confirm process.exit(1) was triggered
    expect(process.exit).toHaveBeenCalledWith(1);

    console.log = originalConsoleLog;
    exitSpy.mockRestore();
  });
});
