// buildGlobalHooks.unit.test.ts
import { describe, it, expect, beforeEach, vi, Mock } from "vitest";
import * as fs from "node:fs/promises";
import * as fsSync from "node:fs";
import * as vite from "vite";
import {
  findFiles,
  getContentHash,
  getGlobalHooksMeta,
  getComponentLibraryConfig,
} from "@embeddable.com/sdk-utils";

import buildGlobalHooks from "../src/buildGlobalHooks";
import { ResolvedEmbeddableConfig } from "./defineConfig";
import path from "node:path";

// Mock implementations
vi.mock("node:fs/promises");
vi.mock("node:fs");
vi.mock("vite");
vi.mock("@embeddable.com/sdk-utils", async () => {
  const actual = await vi.importActual<
    typeof import("@embeddable.com/sdk-utils")
  >("@embeddable.com/sdk-utils");
  return {
    ...actual,
    findFiles: vi.fn(),
    getContentHash: vi.fn(),
    getGlobalHooksMeta: vi.fn(),
    getComponentLibraryConfig: vi.fn(),
  };
});

const lifecyclePath = path.resolve(
  process.cwd(),
  "fake",
  "root",
  "embeddable.lifecycle.ts",
);
const themePath = path.resolve(
  process.cwd(),
  "fake",
  "root",
  "embeddable.theme.ts",
);

describe("buildGlobalHooks (Unit Tests)", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it("should skip lifecycle building if file doesn't exist", async () => {
    vi.spyOn(fsSync, "existsSync").mockImplementation((p: any) => {
      // We want the code to see that /fake/root/embeddable.lifecycle.ts does NOT exist
      if (p === lifecyclePath) {
        return false;
      }
      // Otherwise, default to false
      return false;
    });

    // Possibly not used, but let's mock anyway
    (findFiles as Mock).mockResolvedValue([]);

    // The aggregator template might be read if there's some library with a theme
    (fs.readFile as Mock).mockResolvedValue("some template content");
    (getContentHash as Mock).mockReturnValue("abc123");

    const ctx: ResolvedEmbeddableConfig = {
      client: {
        srcDir: path.resolve(process.cwd(), "fake", "src"),
        buildDir: path.resolve(process.cwd(), "fake", "build"),
        rootDir: path.resolve(process.cwd(), "fake", "root"),
        lifecycleHooksFile: lifecyclePath,
        componentLibraries: [],
        customizationFile: themePath,
      },
      core: {
        templatesDir: "/fake/templates",
      },
    } as any;

    await buildGlobalHooks(ctx);

    // Because we skip building the repo lifecycle, no call to vite.build with that entry
    expect(vite.build).not.toHaveBeenCalledWith(
      expect.objectContaining({
        build: expect.objectContaining({
          lib: expect.objectContaining({
            entry: lifecyclePath,
          }),
        }),
      }),
    );
  });

  it("should build lifecycle file if it exists", async () => {
    // Now we say "repo lifecycle does exist"
    vi.spyOn(fsSync, "existsSync").mockImplementation((p: any) => {
      // If path is exactly the lifecycle file => true
      return p === lifecyclePath;
    });

    // Just in case, but not strictly used here
    (findFiles as Mock).mockResolvedValue([]);
    // The aggregator template might or might not be read
    (fs.readFile as Mock).mockResolvedValue("some file content");
    (getContentHash as Mock).mockReturnValue("abc123");
    (fs.rename as Mock).mockResolvedValue(undefined);
    (vite.build as Mock).mockResolvedValue(undefined);

    const ctx: ResolvedEmbeddableConfig = {
      client: {
        srcDir: path.resolve(process.cwd(), "fake", "src"),
        buildDir: path.resolve(process.cwd(), "fake", "build"),
        rootDir: path.resolve(process.cwd(), "fake", "root"),
        lifecycleHooksFile: lifecyclePath,
        customizationFile: themePath,
        componentLibraries: [],
      },
      core: {
        templatesDir: "/fake/templates",
      },
    } as any;

    await buildGlobalHooks(ctx);

    // We expect a call to build the lifecycle
    expect(vite.build).toHaveBeenCalledWith(
      expect.objectContaining({
        build: expect.objectContaining({
          lib: expect.objectContaining({
            entry: lifecyclePath,
            fileName: "embeddable-lifecycle",
          }),
        }),
      }),
    );
  });

  it("should build theme aggregator if libraries exist with themeProvider references", async () => {
    // aggregator template read is guaranteed
    (fs.readFile as Mock).mockResolvedValue("template content");
    (getContentHash as Mock).mockReturnValue("xyz777");
    (fs.rename as Mock).mockResolvedValue(undefined);
    (vite.build as Mock).mockResolvedValue(undefined);

    // Suppose we skip the lifecycle, but aggregator is still built
    vi.spyOn(fsSync, "existsSync").mockImplementation((p: any) => {
      // lifecycle => false
      if (p === lifecyclePath) return false;
      // local theme => true
      if (p === themePath) return true;
      return false;
    });

    (getComponentLibraryConfig as Mock).mockImplementation((cfg: any) => ({
      libraryName: cfg.name,
    }));

    // Each library call to getGlobalHooksMeta occurs twice:
    // 1) buildThemeHook -> aggregator
    // 2) buildLifecycleHooks -> but we skip it if lifecycle doesn't exist
    // The code still calls getGlobalHooksMeta for each library in buildLifecycleHooks
    // So we must provide 4 total mock results for 2 libraries => aggregator + lifecycle each.

    (getGlobalHooksMeta as Mock)
      // aggregator call #1: libA
      .mockResolvedValueOnce({
        themeProvider: "libA-theme.js",
        lifecycleHooks: [],
      })
      // aggregator call #2: libB
      .mockResolvedValueOnce({
        themeProvider: "libB-theme.js",
        lifecycleHooks: [],
      })
      // lifecycle call #1: libA
      .mockResolvedValueOnce({
        themeProvider: "libA-theme.js",
        lifecycleHooks: [],
      })
      // lifecycle call #2: libB
      .mockResolvedValueOnce({
        themeProvider: "libB-theme.js",
        lifecycleHooks: [],
      });

    const ctx: ResolvedEmbeddableConfig = {
      client: {
        srcDir: path.resolve(process.cwd(), "fake", "src"),
        buildDir: path.resolve(process.cwd(), "fake", "build"),
        rootDir: path.resolve(process.cwd(), "fake", "root"),
        lifecycleHooksFile: lifecyclePath,
        customizationFile: themePath,
        componentLibraries: [{ name: "libA" }, { name: "libB" }],
      },
      core: {
        templatesDir: "/fake/templates",
      },
    } as any;

    await buildGlobalHooks(ctx);

    // aggregator => build with entry = /fake/build/embeddableThemeHook.js
    expect(vite.build).toHaveBeenCalledWith(
      expect.objectContaining({
        build: expect.objectContaining({
          lib: expect.objectContaining({
            entry: expect.stringContaining("embeddableThemeHook.js"),
            fileName: `embeddable-theme-xyz777`, // from getContentHash
          }),
        }),
      }),
    );
  });

  it("should skip aggregator if no library has themeProvider and no local theme", async () => {
    (fs.readFile as Mock).mockResolvedValue("template content");
    (getContentHash as Mock).mockReturnValue("someHash");
    (fs.rename as Mock).mockResolvedValue(undefined);
    (vite.build as Mock).mockResolvedValue(undefined);

    vi.spyOn(fsSync, "existsSync").mockImplementation((p: any) => {
      // Suppose no local theme => false
      if (p === themePath) return false;
      if (p === lifecyclePath) return false;
      return false;
    });

    // Each library is missing a themeProvider
    (getComponentLibraryConfig as Mock).mockImplementation((cfg: any) => ({
      libraryName: cfg.name,
    }));
    // no theme, so aggregator is skipped
    (getGlobalHooksMeta as Mock).mockResolvedValue({
      themeProvider: null,
      lifecycleHooks: [],
    });

    const ctx: ResolvedEmbeddableConfig = {
      client: {
        srcDir: path.resolve(process.cwd(), "fake", "src"),
        buildDir: path.resolve(process.cwd(), "fake", "build"),
        rootDir: path.resolve(process.cwd(), "fake", "root"),
        lifecycleHooksFile: lifecyclePath,
        customizationFile: themePath,
        componentLibraries: [
          { name: "libA" }, // no theme
        ],
      },
      core: {
        templatesDir: "/fake/templates",
      },
    } as any;

    await buildGlobalHooks(ctx);

    // aggregator not built
    // We do an exact check: "not toHaveBeenCalledWith"
    // But the code might still do a build for the lifecycle if it existed.
    // We said the lifecycle is false => so no build for aggregator
    expect(vite.build).not.toHaveBeenCalledWith(
      expect.objectContaining({
        build: expect.objectContaining({
          lib: expect.objectContaining({
            entry: expect.stringContaining("embeddableThemeHook.js"),
          }),
        }),
      }),
    );
  });
});
