import { describe, it, expect, afterEach, beforeAll, afterAll } from "vitest";
import Fastify from "fastify";
import cookie from "@fastify/cookie";
import rateLimit from "@fastify/rate-limit";
import jwt from "jsonwebtoken";
import prisma from "../config/database";
import { config } from "../config/env";
import ordersRoutes from "../routes/admin/orders";
import {
  createOrder,
  listOrders,
  getOrder,
  getOrderAttachment,
} from "../services/orders.service";

const createdOrderIds: number[] = [];
const createdCustomerIds: number[] = [];

afterEach(async () => {
  for (const id of createdOrderIds)
    await prisma.orders.deleteMany({ where: { id } });
  for (const id of createdCustomerIds)
    await prisma.customers.deleteMany({ where: { id } });
  createdOrderIds.length = 0;
  createdCustomerIds.length = 0;
  await prisma.number_sequences.deleteMany({ where: { type: "shared" } });
});

const baseOrder = {
  status: "prijata" as const,
  currency: "CZK",
  language: "cs",
  exchange_rate: 1,
};

function failResult(res: unknown): never {
  throw new Error(`expected a success result, got ${JSON.stringify(res)}`);
}

async function makeCustomer() {
  const c = await prisma.customers.create({
    data: { name: "Odběratel a.s." },
  });
  createdCustomerIds.push(c.id);
  return c;
}

const baseListParams = {
  page: 1,
  limit: 50,
  skip: 0,
  sort: "id",
  order: "desc" as const,
};

describe("listOrders search filter", () => {
  it("returns the order when search matches its order_number", async () => {
    const customer = await makeCustomer();
    const res = await createOrder({
      ...baseOrder,
      customer_id: customer.id,
      create_project: false,
    });
    if (!("id" in res)) failResult(res);
    createdOrderIds.push(res.id!);

    const list = await listOrders({
      ...baseListParams,
      search: res.order_number!,
    });
    expect(list.data.map((o) => o.id)).toContain(res.id);
  });

  it("excludes the order when search matches nothing", async () => {
    const customer = await makeCustomer();
    const res = await createOrder({
      ...baseOrder,
      customer_id: customer.id,
      create_project: false,
    });
    if (!("id" in res)) failResult(res);
    createdOrderIds.push(res.id!);

    const list = await listOrders({
      ...baseListParams,
      search: "ZZZ-NO-SUCH-ORDER-ZZZ",
    });
    expect(list.data.map((o) => o.id)).not.toContain(res.id);
  });
});

describe("order attachment payload hygiene", () => {
  // The PO attachment blob must be served ONLY by GET /:id/attachment —
  // list/detail payloads carry attachment_name as the existence signal.
  const PDF_BYTES = Buffer.from("%PDF-1.4 orders-list-test-attachment-bytes");
  const TEST_ADMIN = "orders_list_test_admin";

  let app: Awaited<ReturnType<typeof buildOrdersApp>>;
  let adminToken: string;
  let adminUserId: number;

  async function buildOrdersApp() {
    const fastify = Fastify({ logger: false });
    await fastify.register(cookie);
    await fastify.register(rateLimit, { max: 1000, timeWindow: "1 minute" });
    // multipart is registered inside ordersRoutes itself
    await fastify.register(ordersRoutes, { prefix: "/api/admin/orders" });
    return fastify;
  }

  beforeAll(async () => {
    app = await buildOrdersApp();

    // Clean up any leftover test user from a previous failed run
    await prisma.users.deleteMany({ where: { username: TEST_ADMIN } });

    const adminRole = await prisma.roles.findFirst({
      where: { name: "admin" },
    });
    if (!adminRole)
      throw new Error("Admin role not found — seed the database first");

    const adminUser = await prisma.users.create({
      data: {
        username: TEST_ADMIN,
        email: `${TEST_ADMIN}@test.local`,
        password_hash:
          "$2a$12$LJ3m4ys3Lg4oLBFnYP2amuPBzJnJBbGzCl5Y6X9Y8r0q5.s3L6OyO",
        first_name: "Orders",
        last_name: "ListTest",
        is_active: true,
        role_id: adminRole.id,
      },
    });
    adminUserId = adminUser.id;
    adminToken = jwt.sign(
      { sub: adminUser.id, username: adminUser.username, role: "admin" },
      config.jwt.secret,
      { expiresIn: "15m" },
    );
  });

  afterAll(async () => {
    await prisma.users.deleteMany({ where: { id: adminUserId } });
    await app.close();
  });

  /** One order WITH a stored PO attachment, one WITHOUT. */
  async function makeOrderPair() {
    const customer = await makeCustomer();
    const withRes = await createOrder(
      { ...baseOrder, customer_id: customer.id, create_project: false },
      PDF_BYTES,
      "po-test.pdf",
    );
    if (!("id" in withRes)) failResult(withRes);
    createdOrderIds.push(withRes.id!);

    const withoutRes = await createOrder({
      ...baseOrder,
      customer_id: customer.id,
      create_project: false,
    });
    if (!("id" in withoutRes)) failResult(withoutRes);
    createdOrderIds.push(withoutRes.id!);

    return { withId: withRes.id!, withoutId: withoutRes.id! };
  }

  it("listOrders rows never contain attachment_data; attachment_name signals existence", async () => {
    const { withId, withoutId } = await makeOrderPair();

    const list = await listOrders(baseListParams);
    const withRow = list.data.find((o) => o.id === withId);
    const withoutRow = list.data.find((o) => o.id === withoutId);
    expect(withRow).toBeTruthy();
    expect(withoutRow).toBeTruthy();

    expect("attachment_data" in withRow!).toBe(false);
    expect("attachment_data" in withoutRow!).toBe(false);
    expect(withRow!.attachment_name).toBe("po-test.pdf");
    expect(withoutRow!.attachment_name).toBeNull();
  });

  it("getOrder detail never contains attachment_data but keeps attachment_name", async () => {
    const { withId, withoutId } = await makeOrderPair();

    const withDetail = await getOrder(withId);
    const withoutDetail = await getOrder(withoutId);
    expect(withDetail).toBeTruthy();
    expect(withoutDetail).toBeTruthy();

    expect("attachment_data" in withDetail!).toBe(false);
    expect("attachment_data" in withoutDetail!).toBe(false);
    expect(withDetail!.attachment_name).toBe("po-test.pdf");
    expect(withoutDetail!.attachment_name).toBeNull();
  });

  it("HTTP list/detail JSON carries no attachment_data; /attachment still serves the binary", async () => {
    const { withId, withoutId } = await makeOrderPair();
    const headers = { authorization: `Bearer ${adminToken}` };

    const listRes = await app.inject({
      method: "GET",
      url: "/api/admin/orders?sort=id&order=desc&per_page=100",
      headers,
    });
    expect(listRes.statusCode).toBe(200);
    const listJson = JSON.parse(listRes.body);
    expect(listJson.data.map((o: { id: number }) => o.id)).toContain(withId);
    expect(listRes.body).not.toContain("attachment_data");

    const detailRes = await app.inject({
      method: "GET",
      url: `/api/admin/orders/${withId}`,
      headers,
    });
    expect(detailRes.statusCode).toBe(200);
    expect(detailRes.body).not.toContain("attachment_data");
    expect(JSON.parse(detailRes.body).data.attachment_name).toBe("po-test.pdf");

    // The dedicated endpoint is the ONE place the blob is served — byte-exact.
    const attRes = await app.inject({
      method: "GET",
      url: `/api/admin/orders/${withId}/attachment`,
      headers,
    });
    expect(attRes.statusCode).toBe(200);
    expect(attRes.headers["content-type"]).toContain("application/pdf");
    expect(Buffer.from(attRes.rawPayload).equals(PDF_BYTES)).toBe(true);

    const noAttRes = await app.inject({
      method: "GET",
      url: `/api/admin/orders/${withoutId}/attachment`,
      headers,
    });
    expect(noAttRes.statusCode).toBe(404);
  });

  it("getOrderAttachment round-trips the stored bytes (and null without one)", async () => {
    const { withId, withoutId } = await makeOrderPair();

    const att = await getOrderAttachment(withId);
    expect(att).toBeTruthy();
    expect(att!.filename).toBe("po-test.pdf");
    expect(att!.data.equals(PDF_BYTES)).toBe(true);

    expect(await getOrderAttachment(withoutId)).toBeNull();
  });
});

describe("listOrders month filter", () => {
  it("returns only orders whose created_at is in the given month", async () => {
    const customer = await makeCustomer();
    const res = await createOrder({
      ...baseOrder,
      customer_id: customer.id,
      create_project: false,
    });
    if (!("id" in res)) failResult(res);
    createdOrderIds.push(res.id!);

    // Pin created_at to a known month so the range filter is deterministic.
    await prisma.orders.update({
      where: { id: res.id },
      data: { created_at: new Date(2026, 2, 15, 12, 0, 0) },
    });

    const inMonth = await listOrders({
      ...baseListParams,
      month: 3,
      year: 2026,
    });
    expect(inMonth.data.map((o) => o.id)).toContain(res.id);

    const otherMonth = await listOrders({
      ...baseListParams,
      month: 4,
      year: 2026,
    });
    expect(otherMonth.data.map((o) => o.id)).not.toContain(res.id);
  });
});
