import { describe, it, expect, vi, beforeEach, afterEach, Mock } from "vitest";
import * as http from "node:http";
import axios from "axios";
import provideConfig from "./provideConfig";
import buildGlobalHooks from "./buildGlobalHooks";
import { getToken } from "./login";
import * as chokidar from "chokidar";
import dev from "./dev";
import { checkNodeVersion } from "./utils";
import { createManifest } from "./cleanup";
import prepare from "./prepare";
import { WebSocketServer } from "ws";
import * as open from "open";
import { logError } from "./logger";
import { ResolvedEmbeddableConfig } from "./defineConfig";
import { RollupWatcher } from "rollup";
import build from "./build";
// Mock dependencies
vi.mock("./buildTypes", () => ({ default: vi.fn() }));
vi.mock("./buildGlobalHooks", () => ({ default: vi.fn() }));
vi.mock("./prepare", () => ({ default: vi.fn(), removeIfExists: vi.fn() }));
vi.mock("./generate", () => ({ default: vi.fn() }));
vi.mock("./provideConfig", () => ({ default: vi.fn() }));
vi.mock("@stencil/core/sys/node", () => ({
  createNodeLogger: vi.fn(),
  createNodeSys: vi.fn(),
}));
vi.mock("open", () => ({ default: vi.fn() }));
vi.mock("ws", () => ({ WebSocketServer: vi.fn() }));
vi.mock("chokidar", () => ({ watch: vi.fn((_) => ({ on: vi.fn() })) }));
vi.mock("./login", () => ({ getToken: vi.fn(), default: vi.fn() }));
vi.mock("axios", () => ({ default: { get: vi.fn(), post: vi.fn() } }));
vi.mock("@embeddable.com/sdk-utils", () => ({ findFiles: vi.fn() }));
vi.mock("./push", () => ({ archive: vi.fn(), sendBuild: vi.fn() }));
vi.mock("./validate", () => ({ default: vi.fn() }));
vi.mock("./utils", () => ({ checkNodeVersion: vi.fn() }));
vi.mock("./cleanup", () => ({ createManifest: vi.fn() }));
vi.mock("node:http", () => ({
  createServer: vi.fn(() => ({ listen: vi.fn() })),
}));
vi.mock("./logger", () => ({
  initLogger: vi.fn(),
  logError: vi.fn(),
}));

const mockConfig = {
  client: {
    rootDir: "/mock/root",
    buildDir: "/mock/root/.embeddable-dev-build",
    componentDir: "/mock/root/.embeddable-dev-build/component",
    stencilBuild: "/mock/root/.embeddable-dev-build/dist/embeddable-wrapper",
    tmpDir: "/mock/root/.embeddable-dev-tmp",
    globalCss: "/mock/root/global.css",
    modelsSrc: "/mock/root/models",
    presetsSrc: "/mock/root/presets",
    componentLibraries: [],
  },
  plugins: [],
  previewBaseUrl: "http://preview.example.com",
  pushBaseUrl: "http://push.example.com",
};

describe("dev command", () => {
  let listenMock: Mock;
  beforeEach(() => {
    listenMock = vi.fn();
    vi.mocked(http.createServer).mockImplementation(
      () =>
        ({
          listen: listenMock,
        }) as any
    );
    vi.mocked(WebSocketServer).mockImplementation(() => {
      return {
        clients: [],
        on: vi.fn(),
      } as any;
    });

    // Mock process.on to avoid actually setting up process listeners
    vi.spyOn(process, "on").mockImplementation(() => process);
    vi.spyOn(process, "exit").mockImplementation(() => undefined as never);

    vi.mocked(provideConfig).mockResolvedValue(
      mockConfig as unknown as ResolvedEmbeddableConfig
    );
    vi.mocked(getToken).mockResolvedValue("mock-token");
    vi.mocked(axios.get).mockResolvedValue({
      data: [{ workspaceId: "mock-workspace" }],
    });
    vi.mocked(axios.post).mockResolvedValue({
      data: "mock-workspace",
    });

    // @ts-ignore
    const watcherMock: RollupWatcher = { on: vi.fn(), close: vi.fn() };
    vi.mocked(buildGlobalHooks).mockResolvedValue({
      themeWatcher: watcherMock,
      lifecycleWatcher: watcherMock,
    });
  });

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

  it("should set up the development and open workspace page with pushComponents false", async () => {
    vi.mocked(provideConfig).mockResolvedValue({
      ...mockConfig,
      pushComponents: false,
      pushModels: true,
    } as unknown as ResolvedEmbeddableConfig);

    // Run the dev command
    await dev();

    // Verify that the necessary functions were called
    expect(checkNodeVersion).toHaveBeenCalled();
    expect(prepare).toHaveBeenCalled();
    expect(http.createServer).toHaveBeenCalled();
    expect(WebSocketServer).toHaveBeenCalled();

    // Verify that the server was set up to listen on the correct port
    expect(listenMock).toHaveBeenCalledWith(8926, expect.any(Function));

    // Call the listen callback to simulate the server being set up
    listenMock.mock.calls[0][1]();
    expect(createManifest).toHaveBeenCalled();

    await expect.poll(() => chokidar.watch).toBeCalledTimes(1);

    expect(open.default).toHaveBeenCalledWith(
      "http://preview.example.com/workspace/mock-workspace"
    );
  });

  it("should set up the development with pushComponents false", async () => {
    vi.mocked(provideConfig).mockResolvedValue({
      ...mockConfig,
      pushComponents: false,
      pushModels: true,
    } as unknown as ResolvedEmbeddableConfig);

    // Run the dev command
    await dev();

    // Verify that the necessary functions were called
    expect(checkNodeVersion).toHaveBeenCalled();
    expect(prepare).toHaveBeenCalled();
    expect(http.createServer).toHaveBeenCalled();
    expect(WebSocketServer).toHaveBeenCalled();

    // Verify that the server was set up to listen on the correct port
    expect(listenMock).toHaveBeenCalledWith(8926, expect.any(Function));

    // Call the listen callback to simulate the server being set up
    listenMock.mock.calls[0][1]();
    expect(createManifest).toHaveBeenCalled();

    await expect.poll(() => chokidar.watch).toBeCalledTimes(1);
  });

  it("should set up the development environment with pushComponents true", async () => {
    vi.mocked(provideConfig).mockResolvedValue({
      ...mockConfig,
      pushComponents: true,
      pushModels: true,
    } as unknown as ResolvedEmbeddableConfig);

    // Run the dev command
    await dev();

    // Verify that the necessary functions were called
    expect(checkNodeVersion).toHaveBeenCalled();
    expect(prepare).toHaveBeenCalled();
    expect(http.createServer).toHaveBeenCalled();
    expect(WebSocketServer).toHaveBeenCalled();

    // Verify that the server was set up to listen on the correct port
    expect(listenMock).toHaveBeenCalledWith(8926, expect.any(Function));

    // Call the listen callback to simulate the server being set up
    listenMock.mock.calls[0][1]();
    expect(createManifest).toHaveBeenCalled();

    await expect.poll(() => chokidar.watch).toBeCalledTimes(2);
  });

  it("should log errors and exit on failure", async () => {
    const originalConsoleLog = console.log;
    console.log = vi.fn();
    const error = new Error("Test error");
    const exitSpy = vi.spyOn(process, "exit").mockImplementation(() => {
      throw new Error("process.exit");
    });

    vi.mocked(checkNodeVersion).mockImplementation(() => {
      throw error;
    });

    await expect(dev()).rejects.toThrow("process.exit");

    expect(console.log).toHaveBeenCalledWith(error);

    expect(logError).toHaveBeenCalledWith({
      command: "dev",
      breadcrumbs: ["run dev"],
      error,
    });
    expect(exitSpy).toHaveBeenCalledWith(1);

    console.log = originalConsoleLog;
  });
});
