import { test, expect } from "@playwright/test";
import {
  buildContactMessage,
  checkTransportReady,
  escapeHtml,
  readMailConfig,
  resolveTransportKind,
  sanitizeHeader,
  validateContactPayload,
  type MailConfig,
} from "../../src/lib/mail";
import { CONTACT_SUBJECTS } from "../../src/lib/contact-subjects";

const COMPLETE_ENV = {
  SMTP_HOST: "smtp.example.com",
  SMTP_PORT: "465",
  SMTP_USER: "user@example.com",
  SMTP_PASS: "secret",
  SMTP_FROM: "user@example.com",
  CONTACT_TO: "inbox@example.com",
} satisfies NodeJS.ProcessEnv;

const CONFIG: MailConfig = {
  transport: "smtp",
  host: "smtp.example.com",
  port: 465,
  secure: true,
  user: "user@example.com",
  pass: "secret",
  from: "website@example.com",
  to: "inbox@example.com",
  rejectUnauthorized: true,
};

/** Narrow to one variant, failing loudly if the env resolved to the other. */
function smtpConfigFrom(env: NodeJS.ProcessEnv) {
  const result = readMailConfig(env);
  if (!result.ok) throw new Error(`expected a usable config; missing ${result.missing.join(", ")}`);
  if (result.config.transport !== "smtp") throw new Error("expected the smtp transport");
  return result.config;
}

function sendmailConfigFrom(env: NodeJS.ProcessEnv) {
  const result = readMailConfig(env);
  if (!result.ok) throw new Error(`expected a usable config; missing ${result.missing.join(", ")}`);
  if (result.config.transport !== "sendmail") throw new Error("expected the sendmail transport");
  return result.config;
}

test.describe("transport selection", () => {
  test("defaults to the local relay when no SMTP host is configured", () => {
    expect(resolveTransportKind({})).toBe("sendmail");
  });

  test("uses SMTP when a host is configured, so existing boxes keep working", () => {
    expect(resolveTransportKind({ SMTP_HOST: "smtp.example.com" })).toBe("smtp");
  });

  test("MAIL_TRANSPORT overrides the host-based guess in both directions", () => {
    expect(
      resolveTransportKind({ SMTP_HOST: "smtp.example.com", MAIL_TRANSPORT: "sendmail" }),
    ).toBe("sendmail");
    expect(resolveTransportKind({ MAIL_TRANSPORT: "smtp" })).toBe("smtp");
  });

  test("an unrecognised MAIL_TRANSPORT falls back to the host-based rule", () => {
    expect(resolveTransportKind({ MAIL_TRANSPORT: "carrier-pigeon" })).toBe("sendmail");
  });
});

test.describe("readMailConfig: sendmail", () => {
  const ENV = {
    MAIL_TRANSPORT: "sendmail",
    SMTP_FROM: "website@example.com",
    CONTACT_TO: "inbox@example.com",
  } satisfies NodeJS.ProcessEnv;

  // The whole point of the pipe: the app holds no mail credential, so rotating
  // the Titan password never touches this deployment.
  test("needs only the two addresses, and holds no credential", () => {
    const config = sendmailConfigFrom(ENV);
    expect(config.from).toBe("website@example.com");
    expect(config.to).toBe("inbox@example.com");
    expect(JSON.stringify(config).toLowerCase()).not.toContain("pass");
  });

  test("defaults to the standard sendmail path", () => {
    expect(sendmailConfigFrom(ENV).path).toBe("/usr/sbin/sendmail");
  });

  test("honours SENDMAIL_PATH and splits SENDMAIL_ARGS on whitespace", () => {
    const config = sendmailConfigFrom({
      ...ENV,
      SENDMAIL_PATH: "/usr/bin/msmtp",
      SENDMAIL_ARGS: "-a  clearfin",
    });
    expect(config.path).toBe("/usr/bin/msmtp");
    expect(config.args).toEqual(["-a", "clearfin"]);
  });

  // The production failure: the standalone server started with no env at all,
  // and nodemailer quietly fell back to connecting to localhost:465.
  test("reports an entirely empty environment as unconfigured", () => {
    const result = readMailConfig({});
    expect(result.ok).toBe(false);
    if (result.ok) return;
    expect(result.transport).toBe("sendmail");
    expect(result.missing).toEqual(["SMTP_FROM", "CONTACT_TO"]);
  });
});

test.describe("checkTransportReady", () => {
  const sendmailAt = (path: string): MailConfig => ({
    transport: "sendmail",
    path,
    args: [],
    from: "website@example.com",
    to: "inbox@example.com",
  });

  test("passes for a binary that exists and is executable", () => {
    expect(checkTransportReady(sendmailAt(process.execPath)).ok).toBe(true);
  });

  test("fails for a missing binary, naming it and the remedy", () => {
    const result = checkTransportReady(sendmailAt("/nonexistent/sendmail"));
    expect(result.ok).toBe(false);
    if (result.ok) return;
    expect(result.problem).toContain("/nonexistent/sendmail");
    expect(result.problem).toContain("msmtp-mta");
  });

  test("does not probe the network for the SMTP transport", () => {
    expect(checkTransportReady(CONFIG).ok).toBe(true);
  });
});

test.describe("readMailConfig: smtp", () => {
  test("accepts a complete environment", () => {
    const config = smtpConfigFrom(COMPLETE_ENV);
    expect(config.host).toBe("smtp.example.com");
    expect(config.port).toBe(465);
    expect(config.to).toBe("inbox@example.com");
  });

  test("reports every missing variable by name rather than throwing", () => {
    const result = readMailConfig({ SMTP_HOST: "smtp.example.com" });
    expect(result.ok).toBe(false);
    if (result.ok) return;
    expect(result.missing).toEqual(["SMTP_FROM", "CONTACT_TO", "SMTP_USER", "SMTP_PASS"]);
  });

  test("treats whitespace-only values as missing", () => {
    const result = readMailConfig({ ...COMPLETE_ENV, SMTP_PASS: "   " });
    expect(result.ok).toBe(false);
    if (result.ok) return;
    expect(result.missing).toEqual(["SMTP_PASS"]);
  });

  test("defaults the port to 465 when unset", () => {
    const { SMTP_PORT: _omitted, ...rest } = COMPLETE_ENV;
    expect(smtpConfigFrom(rest).port).toBe(465);
  });

  test("rejects a non-numeric port instead of silently using NaN", () => {
    const result = readMailConfig({ ...COMPLETE_ENV, SMTP_PORT: "not-a-port" });
    expect(result.ok).toBe(false);
    if (result.ok) return;
    expect(result.invalid).toEqual(["SMTP_PORT"]);
  });

  test("derives implicit TLS from the port and lets SMTP_SECURE override", () => {
    expect(smtpConfigFrom({ ...COMPLETE_ENV, SMTP_PORT: "465" }).secure).toBe(true);
    expect(smtpConfigFrom({ ...COMPLETE_ENV, SMTP_PORT: "2525" }).secure).toBe(false);
    expect(
      smtpConfigFrom({ ...COMPLETE_ENV, SMTP_PORT: "2525", SMTP_SECURE: "true" }).secure,
    ).toBe(true);
  });

  test("validates TLS certificates unless explicitly told not to", () => {
    expect(smtpConfigFrom(COMPLETE_ENV).rejectUnauthorized).toBe(true);
    expect(
      smtpConfigFrom({ ...COMPLETE_ENV, SMTP_TLS_REJECT_UNAUTHORIZED: "false" })
        .rejectUnauthorized,
    ).toBe(false);
  });
});

test.describe("validateContactPayload", () => {
  const valid = {
    name: "Ada Lovelace",
    email: "ada@example.com",
    phone: "+41 78 000 00 00",
    company: "Analytical Engines",
    subject: "General Inquiry",
    message: "Hello there.",
  };

  test("accepts a complete submission and trims it", () => {
    const result = validateContactPayload({ ...valid, name: "  Ada Lovelace  " });
    expect(result.ok).toBe(true);
    if (!result.ok) return;
    expect(result.value.name).toBe("Ada Lovelace");
  });

  test("accepts a submission without the optional phone number", () => {
    const { phone: _omitted, ...withoutPhone } = valid;
    expect(validateContactPayload(withoutPhone).ok).toBe(true);
  });

  test("accepts every subject the form offers", () => {
    for (const subject of CONTACT_SUBJECTS) {
      expect(validateContactPayload({ ...valid, subject }).ok, `subject: ${subject}`).toBe(true);
    }
  });

  test("rejects a subject the form does not offer", () => {
    const result = validateContactPayload({ ...valid, subject: "Anything I Like" });
    expect(result.ok).toBe(false);
  });

  test("lists every missing required field at once", () => {
    const result = validateContactPayload({ email: "ada@example.com" });
    expect(result.ok).toBe(false);
    if (result.ok) return;
    expect(result.errors).toEqual([
      "Name is required.",
      "Company is required.",
      "Subject is required.",
      "Message is required.",
    ]);
  });

  test("rejects whitespace-only required fields", () => {
    const result = validateContactPayload({ ...valid, message: "   \n  " });
    expect(result.ok).toBe(false);
    if (result.ok) return;
    expect(result.errors).toContain("Message is required.");
  });

  test("rejects a malformed email address", () => {
    for (const email of ["not-an-email", "missing@tld", "two@@at.com", "spaces in@example.com"]) {
      expect(validateContactPayload({ ...valid, email }).ok, `email: ${email}`).toBe(false);
    }
  });

  test("rejects oversized fields", () => {
    const result = validateContactPayload({ ...valid, message: "x".repeat(5001) });
    expect(result.ok).toBe(false);
    if (result.ok) return;
    expect(result.errors).toContain("Message is too long.");
  });

  test("rejects non-object and non-string payloads", () => {
    expect(validateContactPayload(null).ok).toBe(false);
    expect(validateContactPayload("a string").ok).toBe(false);
    expect(validateContactPayload({ ...valid, name: { evil: true } }).ok).toBe(false);
  });
});

test.describe("message building", () => {
  test("escapes HTML metacharacters in every interpolated field", () => {
    const message = buildContactMessage(
      {
        name: '<script>alert("xss")</script>',
        email: "ada@example.com",
        phone: "<b>1</b>",
        company: "Tom & Jerry's",
        subject: "General Inquiry",
        message: "<img src=x onerror=alert(1)>",
      },
      CONFIG,
    );

    expect(message.html).not.toContain("<script>");
    expect(message.html).not.toContain("<img src=x");
    expect(message.html).toContain("&lt;script&gt;");
    expect(message.html).toContain("Tom &amp; Jerry&#39;s");
  });

  test("strips CR/LF so submitted text cannot inject mail headers", () => {
    const message = buildContactMessage(
      {
        name: "Ada\r\nBcc: attacker@example.com",
        email: "ada@example.com\r\nBcc: attacker@example.com",
        phone: "",
        company: "Analytical Engines",
        subject: "General Inquiry",
        message: "Hello.",
      },
      CONFIG,
    );

    expect(message.subject).not.toMatch(/[\r\n]/);
    expect(message.replyTo).not.toMatch(/[\r\n]/);
  });

  test("addresses the message from and to the configured mailboxes", () => {
    const message = buildContactMessage(
      {
        name: "Ada",
        email: "ada@example.com",
        phone: "",
        company: "Analytical Engines",
        subject: "Trading Applications",
        message: "Hello.",
      },
      CONFIG,
    );

    expect(message.to).toBe("inbox@example.com");
    expect(message.from).toContain("website@example.com");
    expect(message.replyTo).toBe("ada@example.com");
    expect(message.subject).toBe("[ClearFin] Trading Applications: Ada (Analytical Engines)");
    expect(message.text).toContain("Phone:   n/a");
  });
});

test.describe("escaping helpers", () => {
  test("escapeHtml covers all five metacharacters", () => {
    expect(escapeHtml(`<>&"'`)).toBe("&lt;&gt;&amp;&quot;&#39;");
  });

  test("escapeHtml escapes the ampersand first, avoiding double-encoding", () => {
    expect(escapeHtml("&lt;")).toBe("&amp;lt;");
  });

  test("sanitizeHeader folds newlines into spaces", () => {
    expect(sanitizeHeader("a\r\nb\nc")).toBe("a b c");
  });
});
