// 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"),
        tmpDir: path.resolve(process.cwd(), "fake", "tmp"),
        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"),
        tmpDir: path.resolve(process.cwd(), "fake", "tmp"),
        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"),
        tmpDir: path.resolve(process.cwd(), "fake", "tmp"),
        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"),
        tmpDir: path.resolve(process.cwd(), "fake", "tmp"),
        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"),
          }),
        }),
      }),
    );
  });

  it("should normalize Windows paths in theme import statements", async () => {
    // Mock template content that has the placeholder for local theme import
    const templateContent = `{{LIBRARY_THEME_IMPORTS}}
{{LOCAL_THEME_IMPORT}}
// rest of template`;

    (fs.readFile as Mock)
      .mockResolvedValueOnce(templateContent) // Template read
      .mockResolvedValueOnce("file content"); // Temp file read for hash

    (getContentHash as Mock).mockReturnValue("hash123");
    (vite.build as Mock).mockResolvedValue(undefined);
    (fs.writeFile as Mock).mockResolvedValue(undefined);
    (fs.rm as Mock).mockResolvedValue(undefined);

    // Simulate Windows-style path with backslashes
    const windowsThemePath =
      "C:\\work\\code\\embeddable-boilerplate\\embeddable.theme.ts";

    vi.spyOn(fsSync, "existsSync").mockImplementation((p: any) => {
      if (p === windowsThemePath) return true; // Theme file exists
      if (p === lifecyclePath) return false; // No lifecycle
      return false;
    });

    (getComponentLibraryConfig as Mock).mockReturnValue({
      libraryName: "testLib",
    });
    (getGlobalHooksMeta as Mock).mockResolvedValue({
      themeProvider: null, // No library theme
      lifecycleHooks: [],
    });

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

    await buildGlobalHooks(ctx);

    // Verify that the temporary file was written with normalized path
    expect(fs.writeFile).toHaveBeenCalledWith(
      expect.stringContaining("embeddableThemeHook.js"),
      expect.stringContaining(
        "C:/work/code/embeddable-boilerplate/embeddable.theme.ts",
      ), // Forward slashes
      "utf8",
    );

    // Verify that the content does NOT contain backslashes (which would break imports)
    const writeFileCall = (fs.writeFile as Mock).mock.calls.find((call) =>
      call[0].includes("embeddableThemeHook.js"),
    );
    expect(writeFileCall).toBeDefined();
    expect(writeFileCall![1]).not.toContain("C:\\work\\code"); // Should not have backslashes
  });
});
