import 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 { env } from "../src/config/env.js";
import { prisma } from "../src/db/prisma.js";
import { hashAdminPassword } from "../src/modules/admin/admin-auth.service.js";
import { bucketByDay } from "../src/modules/admin/admin-metrics.service.js";
import { resetDatabase } from "../test-support/reset-db.js";

function buildSignupPayload(): SignupInput {
  const firstName = faker.person.firstName();
  const lastName = faker.person.lastName();
  return {
    firstName,
    lastName,
    email: faker.internet.email({ firstName, lastName }).toLowerCase(),
    password: faker.internet.password({ length: 16 }),
  };
}

async function seedAdmin(): Promise<{ email: string; password: string }> {
  const email = faker.internet.email().toLowerCase();
  const password = faker.internet.password({ length: 16 });
  await prisma.adminUser.create({
    data: { email, name: faker.person.fullName(), passwordHash: await hashAdminPassword(password) },
  });
  return { email, password };
}

/** Retries `check` until it stops throwing or `timeoutMs` elapses — `analytics.recordEvent` writes its row fire-and-forget, so a test observing it has to poll briefly. */
async function eventually(check: () => Promise<void>, timeoutMs = 2000): Promise<void> {
  const start = Date.now();
  for (;;) {
    try {
      await check();
      return;
    } catch (err) {
      if (Date.now() - start > timeoutMs) throw err;
      await new Promise((resolve) => setTimeout(resolve, 50));
    }
  }
}

const adminSecretConfigured = env.ADMIN_JWT_SECRET !== undefined;

describe("Admin metrics", () => {
  describe("bucketByDay (pure)", () => {
    const since = new Date("2026-08-01T00:00:00.000Z");

    it("returns one zero-filled bucket per day, in date order", () => {
      const result = bucketByDay([], since, 3);
      expect(result).to.deep.equal([
        { date: "2026-08-01", count: 0 },
        { date: "2026-08-02", count: 0 },
        { date: "2026-08-03", count: 0 },
      ]);
    });

    it("counts dates into their UTC day and ignores dates outside the window", () => {
      const result = bucketByDay(
        [
          new Date("2026-08-01T09:00:00Z"),
          new Date("2026-08-01T23:30:00Z"),
          new Date("2026-08-03T00:00:00Z"),
          new Date("2026-07-31T23:59:59Z"), // before the window
          new Date("2026-08-10T00:00:00Z"), // after the window
        ],
        since,
        3,
      );
      expect(result).to.deep.equal([
        { date: "2026-08-01", count: 2 },
        { date: "2026-08-02", count: 0 },
        { date: "2026-08-03", count: 1 },
      ]);
    });
  });

  describe("GET /admin/metrics", () => {
    const app = createApp();

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

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

    it("rejects a request with no admin session with 401", async () => {
      const res = await request(app).get("/admin/metrics");
      expect(res.status).to.equal(401);
    });

    it("returns a snapshot reflecting seeded data, plus zero-filled series", async function () {
      if (!adminSecretConfigured) {
        // biome-ignore lint/suspicious/noExplicitAny: mocha's `this.skip()` isn't typed here.
        (this as any).skip();
        return;
      }
      const { email, password } = await seedAdmin();

      // Two end users sign up (also emits `user.signup` analytics events).
      const userA = request.agent(app);
      const userB = request.agent(app);
      await userA.post("/auth/signup").send(buildSignupPayload());
      await userB.post("/auth/signup").send(buildSignupPayload());

      const adminAgent = request.agent(app);
      await adminAgent.post("/admin/auth/login").send({ email, password });

      const res = await adminAgent.get("/admin/metrics").query({ days: 14 });
      expect(res.status).to.equal(200);
      expect(res.body.rangeDays).to.equal(14);
      expect(res.body.snapshot.users).to.equal(2);
      expect(res.body.snapshot.admins).to.equal(1);
      expect(res.body.snapshot.recipes).to.equal(0);

      // 14 daily buckets, each series zero-filled to that length.
      expect(res.body.series.signups).to.have.length(14);
      expect(
        res.body.series.signups.every((b: { count: number }) => typeof b.count === "number"),
      ).to.equal(true);
      // Two signups today → the last bucket counts them.
      const signupTotal = res.body.series.signups.reduce(
        (sum: number, b: { count: number }) => sum + b.count,
        0,
      );
      expect(signupTotal).to.equal(2);
    });

    it("records a user.signup analytics event (fire-and-forget, never blocks signup)", async () => {
      const agent = request.agent(app);
      const signupRes = await agent.post("/auth/signup").send(buildSignupPayload());
      expect(signupRes.status).to.equal(201);

      // `>= 1`, not `=== 1`: `recordEvent` is fire-and-forget, so an insert
      // from an earlier test's signup could in principle land in this
      // window too — the point here is that the instrumentation fires and
      // the signup itself was never blocked by it.
      await eventually(async () => {
        const count = await prisma.analyticsEvent.count({ where: { type: "user.signup" } });
        expect(count).to.be.greaterThan(0);
      });
    });
  });
});
