import { test, expect, afterEach } from "bun:test";
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
  loadAppConfig,
  resolveProfile,
  loadEnv,
  renderStyles,
  resolveCompatibility,
  validateAppConfig,
  explainAppConfig,
} from "../src/index.ts";

afterEach(() => {
  delete process.env.WRNEXUS_PROFILE;
});

test("compatibility dates pin behavior and reject invalid or future policies", () => {
  expect(
    resolveCompatibility({ compatibilityDate: "2026-08-02", frameworkBehaviour: 1 }),
  ).toMatchObject({ needsUpgrade: false, future: false, effectiveBehaviour: 1 });
  expect(validateAppConfig({ compatibilityDate: "2026-02-31" })).toContainEqual(
    expect.objectContaining({ path: "compatibilityDate", severity: "error" }),
  );
  expect(validateAppConfig({ frameworkBehaviour: 2 })).toContainEqual(
    expect.objectContaining({ severity: "error" }),
  );
});

test("observability config requires bounded sampling and a valid OTLP endpoint", () => {
  expect(validateAppConfig({ observability: { sampleRate: Number.NaN } })).toContainEqual(
    expect.objectContaining({ path: "observability.sampleRate", severity: "error" }),
  );
  expect(
    validateAppConfig({ observability: { exporter: "otlp", endpoint: "collector:4318" } }),
  ).toContainEqual(expect.objectContaining({ path: "observability.endpoint", severity: "error" }));
  expect(
    validateAppConfig({
      observability: {
        exporter: "otlp",
        endpoint: "https://collector.example/v1/traces",
        serviceName: "api",
      },
    }),
  ).toEqual([]);
});

test("resolveProfile: explicit > WRNEXUS_PROFILE > mode default", () => {
  delete process.env.WRNEXUS_PROFILE;
  expect(resolveProfile({ mode: "development" })).toBe("development");
  expect(resolveProfile({ mode: "production" })).toBe("production");
  expect(resolveProfile({ explicit: "uat", mode: "production" })).toBe("uat");
  process.env.WRNEXUS_PROFILE = "test";
  expect(resolveProfile({ mode: "production" })).toBe("test");
  expect(resolveProfile({ explicit: "uat" })).toBe("uat"); // explicit still wins
});

test("loadAppConfig deep-merges the active profile and strips `profiles`", async () => {
  const dir = mkdtempSync(join(tmpdir(), "wire-cfg-"));
  writeFileSync(
    join(dir, "wrnexus.config.mjs"),
    `export default {
      port: 3000,
      db: { driver: "sqlite", url: "file:./dev.db" },
      seo: { title: "Base", siteName: "App" },
      profiles: {
        prod: { db: { driver: "postgres", url: "postgres://prod" }, port: 8080 },
        uat: { seo: { title: "UAT" } },
      },
    };`,
  );

  const dev = await loadAppConfig(dir, "development");
  expect(dev.db).toEqual({ driver: "sqlite", url: "file:./dev.db" });
  expect((dev as { profiles?: unknown }).profiles).toBeUndefined();

  const prod = await loadAppConfig(dir, "prod");
  expect(prod.db).toEqual({ driver: "postgres", url: "postgres://prod" });
  expect(prod.port).toBe(8080);
  expect(prod.seo).toEqual({ title: "Base", siteName: "App" }); // untouched

  const uat = await loadAppConfig(dir, "uat");
  expect(uat.seo).toEqual({ title: "UAT", siteName: "App" }); // deep-merged
  expect(uat.db!.driver).toBe("sqlite");
});

test("config layers compose recursively with deterministic application precedence", async () => {
  const root = mkdtempSync(join(tmpdir(), "wrnexus-layers-"));
  try {
    mkdirSync(join(root, "layers", "base"), { recursive: true });
    mkdirSync(join(root, "layers", "company"), { recursive: true });
    writeFileSync(
      join(root, "layers", "base", "wrnexus.layer.mjs"),
      `export default {
      port: 1000, head: ["<meta name='base'>"], seo: { siteName: "Foundation", title: "Base" },
      profiles: { production: { port: 7000 } }
    }`,
    );
    writeFileSync(
      join(root, "layers", "company", "wrnexus.layer.mjs"),
      `export default {
      extends: ["../base"], head: "<meta name='company'>", seo: { title: "Company" }
    }`,
    );
    writeFileSync(
      join(root, "wrnexus.config.mjs"),
      `export default {
      extends: ["./layers/company"], port: 3000, head: ["<meta name='app'>"], seo: { title: "App" },
      profiles: { production: { port: 8000 } }
    }`,
    );

    const config = await loadAppConfig(root, "production");
    expect(config.port).toBe(8000);
    expect((await loadAppConfig(root, "development")).port).toBe(3000);
    expect(config.seo).toEqual({ siteName: "Foundation", title: "App" });
    expect(config.head).toEqual([
      "<meta name='base'>",
      "<meta name='company'>",
      "<meta name='app'>",
    ]);
    expect(config.extends).toBeUndefined();
    const explained = await explainAppConfig(root, "production");
    expect(explained.sources.filter((source) => source.includes("wrnexus.layer")).length).toBe(2);
  } finally {
    rmSync(root, { recursive: true, force: true });
  }
});

test("config layers resolve packages and reject dependency cycles", async () => {
  const root = mkdtempSync(join(tmpdir(), "wrnexus-package-layer-"));
  try {
    const pkg = join(root, "node_modules", "company-layer");
    mkdirSync(pkg, { recursive: true });
    writeFileSync(join(root, "package.json"), JSON.stringify({ name: "app" }));
    writeFileSync(
      join(pkg, "package.json"),
      JSON.stringify({
        name: "company-layer",
        wrnexus: { layer: "./foundation.mjs" },
      }),
    );
    writeFileSync(join(pkg, "foundation.mjs"), `export default { port: 4400 }`);
    writeFileSync(join(root, "wrnexus.config.mjs"), `export default { extends: "company-layer" }`);
    expect((await loadAppConfig(root)).port).toBe(4400);

    const cycle = join(root, "cycle");
    mkdirSync(cycle);
    writeFileSync(join(cycle, "a.mjs"), `export default { extends: "./b.mjs" }`);
    writeFileSync(join(cycle, "b.mjs"), `export default { extends: "./a.mjs" }`);
    writeFileSync(join(cycle, "wrnexus.config.mjs"), `export default { extends: "./a.mjs" }`);
    await expect(loadAppConfig(cycle)).rejects.toThrow("WRN-CONFIG-LAYER-CYCLE");
  } finally {
    rmSync(root, { recursive: true, force: true });
  }
});

test("loadEnv layers .env files by precedence; real env always wins", () => {
  const dir = mkdtempSync(join(tmpdir(), "wire-env-"));
  writeFileSync(join(dir, ".env"), 'BASE=1\nSHARED=base\n# a comment\nQUOTED="hi there"\n');
  writeFileSync(join(dir, ".env.prod"), "SHARED=prod\nPROD_ONLY=yes\nREALVAR=fromfile\n");
  writeFileSync(join(dir, ".env.prod.local"), "SHARED=local\n");

  process.env.REALVAR = "real"; // simulate a real environment variable
  try {
    const loaded = loadEnv(dir, "prod");
    expect(loaded.BASE).toBe("1");
    expect(process.env.BASE).toBe("1");
    expect(process.env.QUOTED).toBe("hi there"); // quotes stripped
    expect(process.env.SHARED).toBe("local"); // .env.prod.local beats .env.prod beats .env
    expect(process.env.PROD_ONLY).toBe("yes");
    expect(process.env.REALVAR).toBe("real"); // real env NOT overridden by a file
  } finally {
    for (const k of ["BASE", "SHARED", "PROD_ONLY", "QUOTED", "REALVAR"]) delete process.env[k];
  }
});

test("loadAppConfig loads env before evaluating the config module", async () => {
  const dir = mkdtempSync(join(tmpdir(), "wire-config-env-"));
  writeFileSync(join(dir, ".env"), "DATABASE_URL=postgres://pms-from-env\n");
  writeFileSync(
    join(dir, "wrnexus.config.mjs"),
    `export default {
      db: { driver: "postgres", url: process.env.DATABASE_URL },
    };`,
  );

  try {
    delete process.env.DATABASE_URL;
    const config = await loadAppConfig(dir, "development");
    expect(config.db?.url).toBe("postgres://pms-from-env");
  } finally {
    delete process.env.DATABASE_URL;
    rmSync(dir, { recursive: true, force: true });
  }
});

test("Google Fonts CSP supports direct fonts and service-worker fetches", async () => {
  const dir = mkdtempSync(join(tmpdir(), "wrnexus-font-csp-"));
  try {
    writeFileSync(
      join(dir, "wrnexus.config.ts"),
      `export default {
        fonts: { google: [{ family: "Inter" }] },
        security: {
          contentSecurityPolicy: {
            directives: { "connect-src": ["'self'", "https://api.example.com"] }
          }
        }
      };\n`,
    );

    const config = await loadAppConfig(dir, "production");
    const directives =
      config.security?.contentSecurityPolicy === false
        ? undefined
        : config.security?.contentSecurityPolicy?.directives;
    expect(directives?.["style-src"]).toContain("https://fonts.googleapis.com");
    expect(directives?.["font-src"]).toContain("https://fonts.gstatic.com");
    expect(directives?.["connect-src"]).toEqual([
      "'self'",
      "https://api.example.com",
      "https://fonts.gstatic.com",
    ]);
  } finally {
    rmSync(dir, { recursive: true, force: true });
  }
});

test("package-aware styles add relative imports and Tailwind scan sources", async () => {
  const root = mkdtempSync(join(tmpdir(), "wrnexus-styles-"));
  try {
    const appDir = join(root, "app");
    const entry = join(appDir, "styles", "global.css");
    const packageSource = join(root, "packages", "system", "components");
    const packageCss = join(root, "packages", "system", "system.css");
    mkdirSync(join(appDir, "styles"), { recursive: true });
    mkdirSync(packageSource, { recursive: true });
    writeFileSync(entry, '@import "tailwindcss";\n');
    writeFileSync(packageCss, ".system{}\n");
    let wrapper = "";
    const css = await renderStyles(
      {
        entryPath: entry,
        appDir,
        appRoot: root,
        mode: "development",
        sources: [packageSource],
        entries: [packageCss],
      },
      {
        process: async ({ entryPath, originalEntryPath, sources }) => {
          expect(originalEntryPath).toBe(entry);
          expect(sources).toEqual([packageSource]);
          wrapper = readFileSync(entryPath!, "utf8");
          return ".compiled{}";
        },
      },
    );
    expect(css).toBe(".compiled{}");
    expect(wrapper).toContain('@import "../../app/styles/global.css";');
    expect(wrapper).toContain('@source "../../packages/system/components";');
    expect(wrapper).not.toContain(root.replace(/\\/g, "/"));
  } finally {
    rmSync(root, { recursive: true, force: true });
  }
});

test("production style processors fail closed by default", async () => {
  const root = mkdtempSync(join(tmpdir(), "wrnexus-styles-"));
  try {
    const entry = join(root, "global.css");
    writeFileSync(entry, "body{}\n");
    await expect(
      renderStyles(
        { entryPath: entry, appDir: root, appRoot: root, mode: "production" },
        {
          process: () => {
            throw new Error("processor unavailable");
          },
        },
      ),
    ).rejects.toThrow("Stylesheet processing failed");
  } finally {
    rmSync(root, { recursive: true, force: true });
  }
});

test("package CSS entries build without an application stylesheet", async () => {
  const root = mkdtempSync(join(tmpdir(), "wrnexus-package-styles-"));
  try {
    const packageCss = join(root, "packages", "system", "system.css");
    mkdirSync(join(root, "packages", "system"), { recursive: true });
    writeFileSync(packageCss, ".system{}\n");
    let wrapper = "";

    const css = await renderStyles(
      {
        entryPath: null,
        appDir: join(root, "app"),
        appRoot: root,
        mode: "development",
        entries: [packageCss],
      },
      {
        process: async ({ entryPath, originalEntryPath }) => {
          expect(originalEntryPath).toBeNull();
          wrapper = readFileSync(entryPath!, "utf8");
          return ".package-compiled{}";
        },
      },
    );

    expect(css).toBe(".package-compiled{}");
    expect(wrapper).toContain('@import "../../packages/system/system.css";');
  } finally {
    rmSync(root, { recursive: true, force: true });
  }
});
