import dotenv from "dotenv";
import { createDBPool } from "@/internal/datastore/index";
import { EDMService } from "@/v1/services/admin/edm/edm.service";
import { EDMRepository } from "@/internal/edm/edm.repository";
import { resolveEDMAssets } from "@/internal/edm/edm-assets.services";
import { readEDMBundleEntries } from "@/internal/edm/edm-import";
import { renderEDM } from "@/internal/edm/edm-render";
import { scanImages } from "@/internal/edm/edm-assets";

dotenv.config();

/**
 * End-to-end smoke test for the local EDM store.
 *
 * Exercises the real path — database rows, stored objects, rendering — rather
 * than mocks, because the parts most likely to be wrong are exactly the seams
 * the unit fixtures cannot reach: does the version object land where the row
 * says it did, does publishing actually make the worker's object readable, does
 * a template survive a round trip through storage unchanged.
 *
 * Requires:
 *   - a database with the EDM migration applied
 *   - EDM_STORAGE_DRIVER=local  (no GCS bucket needed)
 *
 * Creates real rows and deletes them again at the end. Point it at a local
 * database — it refuses to run against anything that looks like production.
 *
 *   EDM_STORAGE_DRIVER=local tsx src/cmd/smoke-edm.ts
 */

const config = {
  host: process.env.DATABASE_HOST,
  port: parseInt(process.env.DATABASE_PORT || "5432"),
  database: process.env.DATABASE_NAME,
  user: process.env.DATABASE_USER,
  password: process.env.DATABASE_PWD,
  ssl:
    process.env.DATABASE_HOST !== "localhost" &&
    process.env.DATABASE_HOST !== "127.0.0.1"
      ? { rejectUnauthorized: false }
      : undefined,
};

const LOCAL_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "host.docker.internal"]);

let passed = 0;
const failures: string[] = [];

const check = (name: string, condition: boolean, detail = "") => {
  if (condition) {
    passed++;
    console.log(`  ✓ ${name}`);
  } else {
    failures.push(`${name}${detail ? ` — ${detail}` : ""}`);
    console.error(`  ✗ ${name}${detail ? ` — ${detail}` : ""}`);
  }
};

const SAMPLE_HTML = `<!doctype html>
<html xmlns:v="urn:schemas-microsoft-com:vml">
<head><title>Smoke Test EDM</title>
<style>.hero{background-image:url('images/bg.png')}</style></head>
<body>
<!--[if gte mso 9]><v:rect><v:fill src="images/bg.png"/></v:rect><![endif]-->
<table background="images/bg.png"><tr><td>
  <p>Hello {{full_name}}</p>
  {{#if is_member}}<p>Member since {{year}}</p>{{else}}<p>Welcome, guest</p>{{/if}}
  {{#greaterThan points 100}}<p>You have lots of points</p>{{/greaterThan}}
  <p>{{insert nickname 'friend'}}</p>
  <a href="<%asm_group_unsubscribe_raw_url%>">Unsubscribe</a>
</td></tr></table>
</body></html>`;

async function run() {
  if (process.env.EDM_STORAGE_DRIVER !== "local") {
    console.error(
      "Refusing to run: set EDM_STORAGE_DRIVER=local so this writes to disk, not a bucket.",
    );
    process.exit(1);
  }

  if (!LOCAL_HOSTS.has(config.host ?? "")) {
    console.error(
      `Refusing to run against DATABASE_HOST="${config.host}". ` +
        `This creates and deletes real rows — point it at a local database.`,
    );
    process.exit(1);
  }

  console.log(`\n▶ EDM smoke test — ${config.database}@${config.host}, storage=local\n`);

  const db = createDBPool(config);
  const svc = EDMService.getInstance();
  let templateId: string | null = null;
  let folderId: string | null = null;

  try {
    // ── schema ──────────────────────────────────────────────────────────────
    try {
      await EDMRepository.ListFolders(db);
      check("migration applied (edm_folders reachable)", true);
    } catch (err) {
      check(
        "migration applied (edm_folders reachable)",
        false,
        `run the migration first: ${(err as Error).message}`,
      );
      throw err;
    }

    // ── folders ─────────────────────────────────────────────────────────────
    const folder = await EDMRepository.CreateFolder(db, { name: "smoke-test-folder" });
    folderId = folder.id;
    check("folder created", Boolean(folder.id));

    const child = await EDMRepository.CreateFolder(db, {
      name: "smoke-child",
      parentId: folder.id,
    });
    check("nested folder created", child.parent_id === folder.id);

    let cycleRejected = false;
    try {
      await EDMRepository.MoveFolder(db, folder.id, child.id);
    } catch {
      cycleRejected = true;
    }
    check("move into own subtree is refused", cycleRejected);
    await EDMRepository.DeleteFolder(db, child.id);

    // ── create ──────────────────────────────────────────────────────────────
    const created = await svc.createTemplate(db, {
      name: "Smoke Test Template",
      subject: "Hello {{full_name}}",
      html_content: SAMPLE_HTML,
      folder_id: folderId,
      active: 1,
    });

    check("template created", created.success, created.success ? "" : created.message);
    if (!created.success) throw new Error(created.message);
    templateId = created.data.id;

    check("id uses the k- prefix", templateId.startsWith("k-"), templateId);
    check("template has one version", created.data.versions.length === 1);
    check("version is active", created.data.versions[0]?.active === 1);

    // ── round trip through storage ──────────────────────────────────────────
    const fetched = await svc.getTemplate(db, templateId);
    check("template read back", fetched.success);
    if (fetched.success) {
      const version = fetched.data.versions[0];
      check(
        "html survives the storage round trip byte-for-byte",
        version?.html_content === SAMPLE_HTML,
        version?.html_content === SAMPLE_HTML
          ? ""
          : `stored ${version?.html_content?.length ?? 0} vs original ${SAMPLE_HTML.length} bytes`,
      );
      check("subject persisted", version?.subject === "Hello {{full_name}}");
    }

    // ── what the worker will fetch ──────────────────────────────────────────
    const renderable = await svc.getRenderableTemplate(db, templateId);
    check(
      "publish wrote the active object the worker reads",
      renderable.success,
      renderable.success ? "" : renderable.message,
    );

    if (renderable.success) {
      const rendered = renderEDM(renderable.data, {
        full_name: "Rohit",
        is_member: true,
        year: 2019,
        points: 250,
      });

      check("subject rendered", rendered.subject === "Hello Rohit");
      check("variable substituted", rendered.html.includes("Hello Rohit"));
      check("#if took the true branch", rendered.html.includes("Member since 2019"));
      check("#if else branch omitted", !rendered.html.includes("Welcome, guest"));
      check(
        "SendGrid helper #greaterThan works",
        rendered.html.includes("You have lots of points"),
      );
      check("insert fell back to its default", rendered.html.includes("friend"));
      check(
        "Outlook conditional comment preserved",
        rendered.html.includes("<!--[if gte mso 9]>"),
      );
      check(
        "unsubscribe tag preserved for SendGrid to substitute",
        rendered.html.includes("<%asm_group_unsubscribe_raw_url%>"),
      );
    }

    // ── image scanning ──────────────────────────────────────────────────────
    const scan = scanImages(SAMPLE_HTML);
    check(
      "finds images in <style>, VML and background= (not just <img>)",
      scan.unresolved.length === 3,
      `found ${scan.unresolved.length}, expected 3`,
    );

    const resolved = await resolveEDMAssets(SAMPLE_HTML, [
      {
        path: "images/bg.png",
        buffer: Buffer.from("PNGDATA"),
        contentType: "image/png",
      },
    ]);
    check("bundle image uploaded and referenced", resolved.manifest.length === 1);
    check("every relative path rewritten", resolved.unresolved.length === 0);
    check(
      "rewritten html has no relative refs left",
      resolved.report.unresolved.length === 0,
    );

    // ── multi-EDM archive scoping ───────────────────────────────────────────
    const zip = await buildTwoTemplateZip();
    const entries = await readEDMBundleEntries(zip);
    check("multi-template archive parsed", entries.ok);
    if (entries.ok) {
      check("one entry per html", entries.entries.length === 2);
      const welcome = entries.entries.find((e) => e.dir === "welcome");
      const reminder = entries.entries.find((e) => e.dir === "reminder");
      check(
        "each entry scoped to its own images",
        welcome?.images.some((i) => i.buffer.toString() === "WELCOME") === true &&
          reminder?.images.some((i) => i.buffer.toString() === "REMINDER") === true,
      );
    }

    // ── update appends a version ────────────────────────────────────────────
    const updated = await svc.updateTemplate(db, templateId, {
      subject: "Updated {{full_name}}",
    });
    check("template updated", updated.success);
    if (updated.success) {
      check("update appended a version", updated.data.versions.length === 2);
      const active = updated.data.versions.find((v) => v.active === 1);
      check("new version is the active one", active?.subject === "Updated {{full_name}}");
      check(
        "exactly one active version",
        updated.data.versions.filter((v) => v.active === 1).length === 1,
      );
    }
  } catch (err) {
    console.error(`\n  fatal: ${(err as Error).message}`);
  } finally {
    // ── cleanup ─────────────────────────────────────────────────────────────
    try {
      if (templateId) await svc.deleteTemplate(db, templateId);
      if (folderId) await EDMRepository.DeleteFolder(db, folderId);
      console.log("\n  cleaned up test rows.");
    } catch (err) {
      console.error(`  cleanup failed: ${(err as Error).message}`);
    }
    await db.destroy();
  }

  const total = passed + failures.length;
  console.log(`\n── ${passed}/${total} checks passed ──`);
  if (failures.length) {
    console.error("\nFAILED:");
    for (const f of failures) console.error(`  ✗ ${f}`);
    process.exit(1);
  }
  console.log("\nThe local store works end to end: rows, objects, publish and render.\n");
}

/** Two EDMs, each with its own images/ directory, both naming images/hero.png. */
async function buildTwoTemplateZip(): Promise<Buffer> {
  const { execFileSync } = await import("node:child_process");
  const { mkdtempSync, mkdirSync, writeFileSync, readFileSync } = await import("node:fs");
  const os = await import("node:os");
  const path = await import("node:path");

  const dir = mkdtempSync(path.join(os.tmpdir(), "edm-smoke-"));
  for (const [name, marker] of [
    ["welcome", "WELCOME"],
    ["reminder", "REMINDER"],
  ] as const) {
    mkdirSync(path.join(dir, name, "images"), { recursive: true });
    writeFileSync(
      path.join(dir, name, "index.html"),
      `<html><head><title>${name}</title></head><body><img src="images/hero.png"></body></html>`,
    );
    writeFileSync(path.join(dir, name, "images", "hero.png"), marker);
  }

  const zipPath = path.join(dir, "bundle.zip");
  execFileSync("zip", ["-qr", zipPath, "."], { cwd: dir });
  return readFileSync(zipPath);
}

run().catch((err) => {
  console.error(err);
  process.exit(1);
});
