import { ErrorCode, type SignupInput } from "@batch-cooking/shared";
import { faker } from "@faker-js/faker";
import { expect } from "chai";
import request from "supertest";
import { createApp } from "../src/app.js";
import { prisma } from "../src/db/prisma.js";
import { resetDatabase } from "../test-support/reset-db.js";

/**
 * Builds a fresh, fake (never real-looking) signup payload. Called anew per
 * test rather than sharing one module-level constant, so tests never
 * accidentally depend on a specific fixture value and each run exercises
 * different data — closer to how the app actually gets used.
 */
function buildSignupPayload(): SignupInput {
  const firstName = faker.person.firstName();
  const lastName = faker.person.lastName();
  return {
    firstName,
    lastName,
    // Lowercased to match what signupSchema/loginSchema normalize the email
    // to (`.toLowerCase()`) — faker sometimes capitalizes parts of it, and
    // without this the fixture value stops matching what's actually stored.
    email: faker.internet.email({ firstName, lastName }).toLowerCase(),
    password: faker.internet.password({ length: 16 }),
  };
}

describe("Auth", () => {
  const app = createApp();

  beforeEach(async () => {
    await resetDatabase();
  });

  after(async () => {
    await prisma.$disconnect();
  });

  describe("POST /auth/signup", () => {
    it("creates a profile without a household yet, and sets a session cookie", async () => {
      const payload = buildSignupPayload();
      const res = await request(app).post("/auth/signup").send(payload);

      expect(res.status).to.equal(201);
      expect(res.body).to.include({
        firstName: payload.firstName,
        lastName: payload.lastName,
        email: payload.email,
      });
      expect(res.body).to.not.have.property("passwordHash");
      // No household is created at signup anymore — it's an optional
      // onboarding step (create/join/skip), see house.test.ts.
      expect(res.body.houseId).to.equal(null);
      expect(res.headers["set-cookie"]?.[0]).to.include("session=");
    });

    it("rejects a duplicate email with 409 EMAIL_ALREADY_IN_USE", async () => {
      const payload = buildSignupPayload();
      await request(app).post("/auth/signup").send(payload);
      const res = await request(app).post("/auth/signup").send(payload);

      expect(res.status).to.equal(409);
      expect(res.body.code).to.equal(ErrorCode.EMAIL_ALREADY_IN_USE);
    });

    it("rejects an invalid payload with 400 VALIDATION_ERROR", async () => {
      const res = await request(app).post("/auth/signup").send({
        firstName: faker.person.firstName(),
        lastName: faker.person.lastName(),
        email: "not-an-email",
        password: "short",
      });

      expect(res.status).to.equal(400);
      expect(res.body.code).to.equal(ErrorCode.VALIDATION_ERROR);
      expect(res.body.details).to.have.keys(["email", "password"]);
    });
  });

  describe("POST /auth/login", () => {
    let payload: SignupInput;

    beforeEach(async () => {
      payload = buildSignupPayload();
      await request(app).post("/auth/signup").send(payload);
    });

    it("logs in with correct credentials", async () => {
      const res = await request(app)
        .post("/auth/login")
        .send({ email: payload.email, password: payload.password });

      expect(res.status).to.equal(200);
      expect(res.body.email).to.equal(payload.email);
    });

    it("rejects a wrong password with 401 INVALID_CREDENTIALS", async () => {
      const res = await request(app)
        .post("/auth/login")
        .send({ email: payload.email, password: faker.internet.password({ length: 16 }) });

      expect(res.status).to.equal(401);
      expect(res.body.code).to.equal(ErrorCode.INVALID_CREDENTIALS);
    });

    it("rejects an unknown email with 401 INVALID_CREDENTIALS", async () => {
      const res = await request(app)
        .post("/auth/login")
        .send({ email: faker.internet.email(), password: payload.password });

      expect(res.status).to.equal(401);
      expect(res.body.code).to.equal(ErrorCode.INVALID_CREDENTIALS);
    });
  });

  describe("GET /auth/me", () => {
    it("rejects requests without a session cookie with 401 NOT_AUTHENTICATED", async () => {
      const res = await request(app).get("/auth/me");

      expect(res.status).to.equal(401);
      expect(res.body.code).to.equal(ErrorCode.NOT_AUTHENTICATED);
    });

    it("returns the current profile when authenticated", async () => {
      const payload = buildSignupPayload();
      const agent = request.agent(app);
      await agent.post("/auth/signup").send(payload);

      const res = await agent.get("/auth/me");

      expect(res.status).to.equal(200);
      expect(res.body.email).to.equal(payload.email);
    });
  });

  describe("DELETE /auth/me", () => {
    it("rejects requests without a session cookie with 401 NOT_AUTHENTICATED", async () => {
      const res = await request(app).delete("/auth/me").send({ password: "whatever" });

      expect(res.status).to.equal(401);
      expect(res.body.code).to.equal(ErrorCode.NOT_AUTHENTICATED);
    });

    it("rejects a wrong password with 401 INVALID_CREDENTIALS, without deleting the profile", async () => {
      const payload = buildSignupPayload();
      const agent = request.agent(app);
      const signupRes = await agent.post("/auth/signup").send(payload);

      const res = await agent.delete("/auth/me").send({ password: "wrong-password" });

      expect(res.status).to.equal(401);
      expect(res.body.code).to.equal(ErrorCode.INVALID_CREDENTIALS);
      expect(
        await prisma.userProfile.findUnique({ where: { id: signupRes.body.id } }),
      ).to.not.equal(null);
    });

    it("deletes the profile and clears the session cookie", async () => {
      const payload = buildSignupPayload();
      const agent = request.agent(app);
      const signupRes = await agent.post("/auth/signup").send(payload);

      const res = await agent.delete("/auth/me").send({ password: payload.password });

      expect(res.status).to.equal(204);
      expect(await prisma.userProfile.findUnique({ where: { id: signupRes.body.id } })).to.equal(
        null,
      );

      const meRes = await agent.get("/auth/me");
      expect(meRes.status).to.equal(401);
    });

    it("deletes the household along with the account when it's the sole member", async () => {
      const payload = buildSignupPayload();
      const agent = request.agent(app);
      await agent.post("/auth/signup").send(payload);
      const houseRes = await agent.post("/house").send({ name: "Chez moi" });

      const res = await agent.delete("/auth/me").send({ password: payload.password });

      expect(res.status).to.equal(204);
      expect(await prisma.house.findUnique({ where: { id: houseRes.body.id } })).to.equal(null);
    });

    it("transfers adminship to another member before deleting an admin's account", async () => {
      const adminPayload = buildSignupPayload();
      const adminAgent = request.agent(app);
      const houseRes = await adminAgent
        .post("/auth/signup")
        .send(adminPayload)
        .then(() => adminAgent.post("/house").send({ name: "Chez nous" }));

      const memberPayload = buildSignupPayload();
      const memberAgent = request.agent(app);
      const memberSignupRes = await memberAgent.post("/auth/signup").send(memberPayload);
      await memberAgent.post("/house/join").send({ inviteCode: houseRes.body.inviteCode });

      const res = await adminAgent.delete("/auth/me").send({ password: adminPayload.password });

      expect(res.status).to.equal(204);
      const house = await prisma.house.findUnique({ where: { id: houseRes.body.id } });
      expect(house?.adminId).to.equal(memberSignupRes.body.id);
    });
  });
});
