import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { hits } from "./contact.utils.js";
import { POST, _resetTransporter } from "./contact.js";

const mockSendMail = vi.fn();

vi.mock("nodemailer", () => ({
  default: {
    createTransport: vi.fn(() => ({ sendMail: mockSendMail })),
  },
}));

function makeRequest(body: unknown): Request {
  return new Request("http://localhost/api/contact", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });
}

async function call(body: unknown, ip = "1.2.3.4"): Promise<Response> {
  return (await POST({ request: makeRequest(body), clientAddress: ip } as never)) as Response;
}

const validBody = {
  firstName: "Jean",
  lastName: "Dupont",
  email: "jean@example.com",
  message: "Ceci est un message suffisamment long.",
};

beforeEach(() => {
  hits.clear();
  mockSendMail.mockReset();
  _resetTransporter();
  delete process.env.SMTP_USER;
  delete process.env.SMTP_PASSWORD;
  delete process.env.SMTP_FROM;
  delete process.env.CONTACT_TO;
});

describe("POST /api/contact - rate limit", () => {
  it("retourne 429 après dépassement du rate limit", async () => {
    process.env.SMTP_USER = "u@test.com";
    process.env.SMTP_PASSWORD = "pass";
    mockSendMail.mockResolvedValue({});
    for (let i = 0; i < 4; i++) await call(validBody, "9.9.9.9");
    const res = await call(validBody, "9.9.9.9");
    expect(res.status).toBe(429);
  });

  it("n'affecte pas une IP différente", async () => {
    process.env.SMTP_USER = "u@test.com";
    process.env.SMTP_PASSWORD = "pass";
    mockSendMail.mockResolvedValue({});
    for (let i = 0; i < 5; i++) await call(validBody, "8.8.8.8");
    const res = await call(validBody, "7.7.7.7");
    expect(res.status).not.toBe(429);
  });

  it("utilise 'unknown' comme IP si clientAddress est vide", async () => {
    process.env.SMTP_USER = "u@test.com";
    process.env.SMTP_PASSWORD = "pass";
    mockSendMail.mockResolvedValue({});
    const res = (await POST({ request: makeRequest(validBody), clientAddress: "" } as never)) as Response;
    expect(res.status).not.toBe(429);
  });
});

describe("POST /api/contact - parsing", () => {
  it("retourne 400 pour un corps JSON invalide", async () => {
    const req = new Request("http://localhost/api/contact", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: "not-json",
    });
    const res = (await POST({ request: req, clientAddress: "1.2.3.4" } as never)) as Response;
    expect(res.status).toBe(400);
  });
});

describe("POST /api/contact - honeypot", () => {
  it("retourne 200 silencieux si le champ website est rempli", async () => {
    const res = await call({ ...validBody, website: "http://spam.com" });
    expect(res.status).toBe(200);
    const json = await res.json();
    expect(json.ok).toBe(true);
  });
});

describe("POST /api/contact - validation", () => {
  it.each([
    [{ ...validBody, firstName: "X" }, "Prenom invalide."],
    [{ ...validBody, lastName: "Y" }, "Nom invalide."],
    [{ ...validBody, email: "pas-un-email" }, "Email invalide."],
    [{ ...validBody, message: "Court" }, "Message trop court (min. 10 caracteres)."],
  ])("retourne 400 avec le bon message d'erreur", async (body, expectedError) => {
    const res = await call(body);
    expect(res.status).toBe(400);
    const json = await res.json();
    expect(json.error).toBe(expectedError);
  });
});

describe("POST /api/contact - envoi email", () => {
  beforeEach(() => {
    process.env.SMTP_USER = "user@test.com";
    process.env.SMTP_PASSWORD = "secret";
    process.env.SMTP_FROM = "noreply@test.com";
    process.env.CONTACT_TO = "admin@test.com";
  });

  afterEach(() => {
    delete process.env.SMTP_USER;
    delete process.env.SMTP_PASSWORD;
    delete process.env.SMTP_FROM;
    delete process.env.CONTACT_TO;
  });

  it("retourne 503 si SMTP non configuré", async () => {
    delete process.env.SMTP_USER;
    delete process.env.SMTP_PASSWORD;
    const res = await call(validBody);
    expect(res.status).toBe(503);
  });

  it("retourne 200 si l'email est envoyé avec succès", async () => {
    mockSendMail.mockResolvedValue({ messageId: "test-id" });
    const res = await call(validBody);
    expect(res.status).toBe(200);
    const json = await res.json();
    expect(json.ok).toBe(true);
  });

  it("retourne 500 si sendMail échoue", async () => {
    mockSendMail.mockRejectedValue(new Error("SMTP connection failed"));
    const res = await call(validBody);
    expect(res.status).toBe(500);
  });

  it("inclut les champs optionnels company et phone dans l'email", async () => {
    mockSendMail.mockResolvedValue({});
    const res = await call({ ...validBody, company: "ACME Corp", phone: "0612345678" });
    expect(res.status).toBe(200);
    const sentArgs = mockSendMail.mock.calls[0][0];
    expect(sentArgs.html).toContain("ACME Corp");
    expect(sentArgs.html).toContain("0612345678");
  });

  it("envoie l'email avec les bons destinataires", async () => {
    mockSendMail.mockResolvedValue({});
    await call(validBody);
    const sentArgs = mockSendMail.mock.calls[0][0];
    expect(sentArgs.to).toBe("admin@test.com");
    expect(sentArgs.replyTo).toBe("jean@example.com");
  });

  it("utilise SMTP_USER comme expéditeur par défaut si SMTP_FROM absent", async () => {
    delete process.env.SMTP_FROM;
    mockSendMail.mockResolvedValue({});
    await call(validBody);
    const sentArgs = mockSendMail.mock.calls[0][0];
    expect(sentArgs.from).toContain("user@test.com");
  });

  it("ajoute les headers de sécurité dans la réponse", async () => {
    mockSendMail.mockResolvedValue({});
    const res = await call(validBody);
    expect(res.headers.get("X-Content-Type-Options")).toBe("nosniff");
  });
});
