import { test, expect } from "@playwright/test";
import { BASE_PATH } from "../../playwright.config";
import {
  sendmailEnvFor,
  startApp,
  startFakeSendmail,
  validSubmission,
  type FakeSendmail,
  type RunningApp,
} from "../helpers/harness";

/**
 * The transport the servers actually use: hand the message to the local msmtp
 * rather than authenticating to Titan ourselves. The app then stores no mail
 * password at all, so rotating the Titan credential never touches a deployment.
 *
 * msmtp is stood in for by a script that records its argv and stdin — the argv
 * matters, because a mismatch between what nodemailer passes and what msmtp
 * accepts is exactly how this would break in production.
 */

test.describe.configure({ mode: "serial" });

let sendmail: FakeSendmail;
let app: RunningApp;

test.beforeAll(async () => {
  sendmail = startFakeSendmail();
  app = await startApp({ env: sendmailEnvFor(sendmail), basePath: BASE_PATH });
});

test.afterAll(async () => {
  await app?.close();
  sendmail?.cleanup();
});

test.beforeEach(() => sendmail.reset());

test("pipes a submitted enquiry to the local relay", async () => {
  const res = await app.post("/api/contact", validSubmission({ name: "Ada Lovelace" }));

  expect(res.status, app.output()).toBe(200);
  expect(res.json.success).toBe(true);

  const calls = sendmail.invocations();
  expect(calls).toHaveLength(1);
  expect(calls[0].message.header("subject")).toBe(
    "[ClearFin] General Inquiry: Ada Lovelace (Test Corp)",
  );
  expect(calls[0].message.header("reply-to")).toBe("runner@clearfin.test");
  expect(calls[0].message.header("to")).toBe("inbox@clearfin.test");
});

test("invokes the relay the way msmtp expects", async () => {
  await app.post("/api/contact", validSubmission());

  const { args } = sendmail.invocations()[0];

  // nodemailer forces -i, then our args, then the envelope recipients.
  expect(args[0]).toBe("-i");
  expect(args).toContain("-f");
  expect(args[args.indexOf("-f") + 1]).toBe("website@clearfin.test");
  expect(args.at(-1)).toBe("inbox@clearfin.test");
});

test("passes extra relay arguments through, for a multi-account msmtprc", async () => {
  const extra = startFakeSendmail();
  const withAccount = await startApp({
    env: { ...sendmailEnvFor(extra), SENDMAIL_ARGS: "-a clearfin" },
    basePath: BASE_PATH,
  });

  try {
    const res = await withAccount.post("/api/contact", validSubmission());
    expect(res.status, withAccount.output()).toBe(200);

    const { args } = extra.invocations()[0];
    expect(args.join(" ")).toContain("-a clearfin");
    expect(args[0]).toBe("-i");
  } finally {
    await withAccount.close();
    extra.cleanup();
  }
});

test("still validates before invoking the relay", async () => {
  const res = await app.post("/api/contact", { email: "runner@clearfin.test" });

  expect(res.status).toBe(400);
  expect(sendmail.invocations()).toHaveLength(0);
});

test("escapes the HTML part of a piped message", async () => {
  await app.post("/api/contact", validSubmission({ message: '<script>alert("xss")</script>' }));

  const { message } = sendmail.invocations()[0];
  expect(message.html()).not.toContain("<script>");
  expect(message.html()).toContain("&lt;script&gt;");
});

test("health check reports the sendmail transport", async () => {
  const res = await app.get("/api/health/mail");

  expect(res.status).toBe(200);
  expect(res.json).toMatchObject({ ok: true, configured: true, transport: "sendmail" });
});

test.describe("an unusable relay is caught before a visitor finds it", () => {
  test("a missing binary fails the health check with a fixable message", async () => {
    const broken = await startApp({
      env: {
        MAIL_TRANSPORT: "sendmail",
        SENDMAIL_PATH: "/nonexistent/sendmail",
        SMTP_FROM: "website@clearfin.test",
        CONTACT_TO: "inbox@clearfin.test",
      },
      basePath: BASE_PATH,
    });

    try {
      const health = await broken.get("/api/health/mail");
      expect(health.status).toBe(503);
      expect(health.json.problem).toContain("/nonexistent/sendmail");
      expect(health.json.problem).toContain("msmtp-mta");

      // And a submission is a clean 503, not a 500 from a failed spawn.
      const res = await broken.post("/api/contact", validSubmission());
      expect(res.status).toBe(503);
      expect(res.json.error).toContain("temporarily unavailable");
    } finally {
      await broken.close();
    }
  });

  test("a binary the service cannot execute is treated as unusable", async () => {
    const unreadable = startFakeSendmail();
    // Mirrors msmtp installed but not executable by the service user.
    await import("node:fs").then(({ chmodSync }) => chmodSync(unreadable.path, 0o644));

    const app2 = await startApp({ env: sendmailEnvFor(unreadable), basePath: BASE_PATH });
    try {
      const health = await app2.get("/api/health/mail");
      expect(health.status).toBe(503);
      expect(health.json.problem).toContain("not executable");
    } finally {
      await app2.close();
      unreadable.cleanup();
    }
  });
});
