import * as fs from "node:fs/promises";
import * as path from "node:path";
import { describe, it, expect, vi, beforeEach } from "vitest";
import buildTypes, { EMB_TYPE_FILE_REGEX } from "./buildTypes";
import { build } from "vite";
import { ResolvedEmbeddableConfig } from "./defineConfig";
import {
  findFiles,
  getContentHash,
  getComponentLibraryConfig,
} from "@embeddable.com/sdk-utils";

import fg from "fast-glob";

// The same config you already have
const config = {
  client: {
    srcDir: "src",
    rootDir: "root",
    buildDir: "build",
    componentLibraries: [],
  },
  outputOptions: {
    typesEntryPointFilename: "typesEntryPointFilename",
  },
};

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

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

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

vi.mock("node:path", async () => {
  const actualPath =
    await vi.importActual<typeof import("node:path")>("node:path");
  return {
    ...actualPath,
    resolve: vi.fn(),
    relative: vi.fn(),
    join: vi.fn(),
    dirname: vi.fn(),
  };
});

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

vi.mock("vite", () => ({
  build: vi.fn(),
}));

vi.mock("fast-glob", () => ({
  default: {
    sync: vi.fn(),
  },
}));

describe("buildTypes", () => {
  beforeEach(() => {
    vi.clearAllMocks();

    vi.mocked(findFiles).mockResolvedValue([["fileName", "filePath"]]);
    vi.mocked(path.relative).mockReturnValue("relativePath");
    vi.mocked(path.resolve).mockReturnValue("resolvedPath");
    vi.mocked(fs.readFile).mockResolvedValue("fileContent");
    vi.mocked(getContentHash).mockReturnValue("somehash");
    // By default, no libraries => no fast-glob usage
    vi.mocked(fg.sync).mockReturnValue([]);

    // We also default to returning an empty config from getComponentLibraryConfig
    vi.mocked(getComponentLibraryConfig).mockReturnValue({
      libraryName: "",
      include: [],
      exclude: [],
    });
  });

  it("should build types", async () => {
    await buildTypes(config as unknown as ResolvedEmbeddableConfig);

    expect(findFiles).toHaveBeenCalledWith("src", EMB_TYPE_FILE_REGEX);

    expect(build).toHaveBeenCalledWith({
      build: {
        emptyOutDir: false,
        lib: {
          entry: "resolvedPath",
          fileName: "embeddable-types",
          formats: ["es"],
        },
        outDir: "build",
      },
      logLevel: "error",
    });

    expect(fs.readFile).toHaveBeenCalledWith("resolvedPath", "utf8");
    expect(getContentHash).toHaveBeenCalledWith("fileContent");

    expect(startMock.succeed).toHaveBeenCalledWith("Types built completed");

    expect(fs.writeFile).toHaveBeenCalledWith(
      "resolvedPath",
      `import '../relativePath';
import '../relativePath';`,
    );

    expect(fs.rm).toHaveBeenCalledWith("resolvedPath");
  });

  it("should not add hash to the file name when watch is enabled", async () => {
    await buildTypes({
      ...config,
      dev: { watch: true, logger: undefined, sys: undefined },
    } as unknown as ResolvedEmbeddableConfig);

    expect(getContentHash).not.toHaveBeenCalled();
    expect(fs.rename).not.toHaveBeenCalled();
  });
  it("should import types from installed libraries if present", async () => {
    const configWithLibrary = {
      ...config,
      client: {
        ...config.client,
        componentLibraries: [
          {
            /* any library config object */
          },
        ],
      },
    } as unknown as ResolvedEmbeddableConfig;

    // Mock getComponentLibraryConfig => returns libraryName: "my-lib"
    vi.mocked(getComponentLibraryConfig).mockReturnValue({
      libraryName: "my-lib",
      include: [],
      exclude: [],
    });

    vi.mocked(fg.sync).mockReturnValue([
      "/fake/path/node_modules/my-lib/dist/embeddable-types-abc123.js",
      "/fake/path/node_modules/my-lib/dist/embeddable-types-xyz987.js",
    ]);

    // Now run
    await buildTypes(configWithLibrary);

    const written = vi.mocked(fs.writeFile).mock.calls[0][1];

    expect(written).toContain(
      `import 'my-lib/dist/embeddable-types-abc123.js';`,
    );
    expect(written).toContain(
      `import 'my-lib/dist/embeddable-types-xyz987.js';`,
    );
    expect(written).toContain(`import '../relativePath';`);
  });

  it("should throw if an error occurs when loading a library", async () => {
    const configWithLibrary = {
      ...config,
      client: {
        ...config.client,
        componentLibraries: [
          {
            /* any library config object */
          },
        ],
      },
    } as unknown as ResolvedEmbeddableConfig;

    vi.mocked(getComponentLibraryConfig).mockReturnValue({
      libraryName: "my-broken-lib",
      include: [],
      exclude: [],
    });

    vi.mocked(fg.sync).mockImplementation(() => {
      throw new Error("some library error");
    });

    await expect(buildTypes(configWithLibrary)).rejects.toThrow(
      "some library error",
    );
  });
});
