import {
  describe,
  it,
  expect,
  beforeAll,
  afterAll,
  beforeEach,
  afterEach,
  vi,
} 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 warehouseRoutes from "../routes/admin/warehouse";
import { nasInvoicesManager } from "../services/nas-financials-manager";
import { securityHeaders } from "../middleware/security";

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/** Short unique prefix for test data (location codes are VarChar(20)) */
const LOC = "WT"; // prefix for location codes
let counter = 0;
function uniqueCode() {
  return `${LOC}${++counter}`; // e.g. WT1, WT2, ...
}

/** Name prefix for items/categories/suppliers (longer names are fine) */
const N = "wh_test_";

/** Build a Fastify app with warehouse routes and required plugins. */
async function buildApp() {
  const app = Fastify({ logger: false });
  await app.register(cookie);
  await app.register(rateLimit, { max: 1000, timeWindow: "1 minute" });
  // multipart is registered inside warehouseRoutes itself, do not register here
  app.addHook("onRequest", securityHeaders);
  await app.register(warehouseRoutes, { prefix: "/api/admin/warehouse" });
  return app;
}

/** Generate a valid JWT access token for a given user. */
function generateToken(user: {
  id: number;
  username: string;
  roleName: string | null;
}): string {
  return jwt.sign(
    { sub: user.id, username: user.username, role: user.roleName },
    config.jwt.secret,
    { expiresIn: "15m" },
  );
}

/** Create auth header from token. */
function authHeader(token: string) {
  return { Authorization: `Bearer ${token}` };
}

// ---------------------------------------------------------------------------
// Test fixtures: user, role, project
// ---------------------------------------------------------------------------

let app: Awaited<ReturnType<typeof buildApp>>;
let adminToken: string;
let noPermToken: string;
let adminUserId: number;
let noPermUserId: number;
let noPermRoleId: number;
let projectId: number;

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

  // Clean up any leftover test data from a previous failed run
  await prisma.sklad_issue_lines.deleteMany({
    where: { issue: { notes: { contains: N } } },
  });
  await prisma.sklad_issues.deleteMany({
    where: { notes: { contains: N } },
  });
  await prisma.sklad_reservations.deleteMany({
    where: { notes: { contains: N } },
  });
  await prisma.sklad_batches.deleteMany({
    where: { item: { name: { contains: N } } },
  });
  await prisma.sklad_receipt_lines.deleteMany({
    where: { receipt: { notes: { contains: N } } },
  });
  await prisma.sklad_receipts.deleteMany({
    where: { notes: { contains: N } },
  });
  await prisma.sklad_item_locations.deleteMany({
    where: { item: { name: { contains: N } } },
  });
  await prisma.sklad_items.deleteMany({
    where: { name: { contains: N } },
  });
  await prisma.sklad_suppliers.deleteMany({
    where: { name: { contains: N } },
  });
  await prisma.sklad_locations.deleteMany({
    where: { code: { startsWith: LOC } },
  });
  await prisma.sklad_categories.deleteMany({
    where: { name: { contains: N } },
  });
  await prisma.sklad_inventory_lines.deleteMany({
    where: { session: { notes: { contains: N } } },
  });
  await prisma.sklad_inventory_sessions.deleteMany({
    where: { notes: { contains: N } },
  });
  // Delete leftover test users and roles
  await prisma.users
    .deleteMany({ where: { username: { startsWith: N } } })
    .catch(() => {});
  await prisma.roles
    .deleteMany({ where: { name: { startsWith: N } } })
    .catch(() => {});
  await prisma.projects
    .deleteMany({ where: { name: { startsWith: N } } })
    .catch(() => {});

  // Create a test admin user (bypasses all permission checks)
  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: `${N}admin`,
      email: `${N}admin@test.local`,
      password_hash:
        "$2a$12$LJ3m4ys3Lg4oLBFnYP2amuPBzJnJBbGzCl5Y6X9Y8r0q5.s3L6OyO",
      first_name: "Test",
      last_name: "Admin",
      is_active: true,
      role_id: adminRole.id,
    },
  });
  adminUserId = adminUser.id;
  adminToken = generateToken({
    id: adminUser.id,
    username: adminUser.username,
    roleName: "admin",
  });

  // Create a role with NO warehouse permissions for permission check tests
  const noPermRole = await prisma.roles.create({
    data: {
      name: `${N}no_warehouse`,
      display_name: "Test No Warehouse",
      description: "Test role without warehouse permissions",
    },
  });
  noPermRoleId = noPermRole.id;

  const noPermUser = await prisma.users.create({
    data: {
      username: `${N}noperm`,
      email: `${N}noperm@test.local`,
      password_hash:
        "$2a$12$LJ3m4ys3Lg4oLBFnYP2amuPBzJnJBbGzCl5Y6X9Y8r0q5.s3L6OyO",
      first_name: "No",
      last_name: "Perm",
      is_active: true,
      role_id: noPermRole.id,
    },
  });
  noPermUserId = noPermUser.id;
  noPermToken = generateToken({
    id: noPermUser.id,
    username: noPermUser.username,
    roleName: noPermRole.name,
  });

  // Create a test project (needed for issues and reservations)
  const project = await prisma.projects.create({
    data: {
      name: `${N}project`,
      status: "active",
    },
  });
  projectId = project.id;
});

afterAll(async () => {
  // Clean up all test data in reverse dependency order
  await prisma.sklad_issue_lines.deleteMany({
    where: { issue: { notes: { contains: N } } },
  });
  await prisma.sklad_issues.deleteMany({
    where: { notes: { contains: N } },
  });
  await prisma.sklad_reservations.deleteMany({
    where: { notes: { contains: N } },
  });
  // Batches reference receipt_lines, so delete batches first
  await prisma.sklad_batches.deleteMany({
    where: { item: { name: { contains: N } } },
  });
  await prisma.sklad_receipt_lines.deleteMany({
    where: { receipt: { notes: { contains: N } } },
  });
  await prisma.sklad_receipts.deleteMany({
    where: { notes: { contains: N } },
  });
  await prisma.sklad_item_locations.deleteMany({
    where: { item: { name: { contains: N } } },
  });
  await prisma.sklad_items.deleteMany({
    where: { name: { contains: N } },
  });
  await prisma.sklad_suppliers.deleteMany({
    where: { name: { contains: N } },
  });
  await prisma.sklad_locations.deleteMany({
    where: { code: { startsWith: LOC } },
  });
  await prisma.sklad_categories.deleteMany({
    where: { name: { contains: N } },
  });
  await prisma.sklad_inventory_lines.deleteMany({
    where: { session: { notes: { contains: N } } },
  });
  await prisma.sklad_inventory_sessions.deleteMany({
    where: { notes: { contains: N } },
  });

  // Clean up test user and role (guard against beforeAll failure)
  if (noPermUserId)
    await prisma.users.delete({ where: { id: noPermUserId } }).catch(() => {});
  if (noPermRoleId)
    await prisma.roles.delete({ where: { id: noPermRoleId } }).catch(() => {});
  if (adminUserId)
    await prisma.users.delete({ where: { id: adminUserId } }).catch(() => {});

  // Clean up test project
  if (projectId)
    await prisma.projects.delete({ where: { id: projectId } }).catch(() => {});

  if (app) await app.close();
});

// ---------------------------------------------------------------------------
// 1. Category CRUD
// ---------------------------------------------------------------------------

describe("Category CRUD", () => {
  let createdCategoryId: number;

  it("creates a category", async () => {
    const res = await app.inject({
      method: "POST",
      url: "/api/admin/warehouse/categories",
      headers: authHeader(adminToken),
      payload: { name: `${N}cat1`, description: "Test category" },
    });
    expect(res.statusCode).toBe(201);
    const body = res.json();
    expect(body.success).toBe(true);
    expect(body.data.name).toBe(`${N}cat1`);
    createdCategoryId = body.data.id;
  });

  it("lists categories", async () => {
    const res = await app.inject({
      method: "GET",
      url: "/api/admin/warehouse/categories",
      headers: authHeader(adminToken),
    });
    expect(res.statusCode).toBe(200);
    const body = res.json();
    expect(body.success).toBe(true);
    expect(Array.isArray(body.data)).toBe(true);
    const found = body.data.find(
      (c: { id: number }) => c.id === createdCategoryId,
    );
    expect(found).toBeDefined();
  });

  it("updates a category", async () => {
    const res = await app.inject({
      method: "PUT",
      url: `/api/admin/warehouse/categories/${createdCategoryId}`,
      headers: authHeader(adminToken),
      payload: { name: `${N}cat1_updated` },
    });
    expect(res.statusCode).toBe(200);
    const body = res.json();
    expect(body.success).toBe(true);
    expect(body.data.name).toBe(`${N}cat1_updated`);
  });

  it("deletes a category with no items", async () => {
    const res = await app.inject({
      method: "DELETE",
      url: `/api/admin/warehouse/categories/${createdCategoryId}`,
      headers: authHeader(adminToken),
    });
    expect(res.statusCode).toBe(200);
    const body = res.json();
    expect(body.success).toBe(true);
  });

  it("blocks delete if items reference the category", async () => {
    // Create a category and an item referencing it
    const catRes = await app.inject({
      method: "POST",
      url: "/api/admin/warehouse/categories",
      headers: authHeader(adminToken),
      payload: { name: `${N}cat_blocked` },
    });
    const catId = catRes.json().data.id;

    await app.inject({
      method: "POST",
      url: "/api/admin/warehouse/items",
      headers: authHeader(adminToken),
      payload: {
        name: `${N}item_cat_block`,
        unit: "ks",
        category_id: catId,
      },
    });

    const delRes = await app.inject({
      method: "DELETE",
      url: `/api/admin/warehouse/categories/${catId}`,
      headers: authHeader(adminToken),
    });
    expect(delRes.statusCode).toBe(409);
    const body = delRes.json();
    expect(body.success).toBe(false);
  });

  it("returns 404 for deleting nonexistent category", async () => {
    const res = await app.inject({
      method: "DELETE",
      url: "/api/admin/warehouse/categories/999999",
      headers: authHeader(adminToken),
    });
    expect(res.statusCode).toBe(404);
  });
});

// ---------------------------------------------------------------------------
// 2. Supplier CRUD
// ---------------------------------------------------------------------------

describe("Supplier CRUD", () => {
  let createdSupplierId: number;

  it("creates a supplier", async () => {
    const res = await app.inject({
      method: "POST",
      url: "/api/admin/warehouse/suppliers",
      headers: authHeader(adminToken),
      payload: { name: `${N}supplier1`, ico: "12345678" },
    });
    expect(res.statusCode).toBe(201);
    const body = res.json();
    expect(body.success).toBe(true);
    expect(body.data.name).toBe(`${N}supplier1`);
    expect(body.data.is_active).toBe(true);
    createdSupplierId = body.data.id;
  });

  it("lists suppliers", async () => {
    const res = await app.inject({
      method: "GET",
      url: "/api/admin/warehouse/suppliers",
      headers: authHeader(adminToken),
    });
    expect(res.statusCode).toBe(200);
    const body = res.json();
    expect(body.success).toBe(true);
    expect(Array.isArray(body.data)).toBe(true);
  });

  it("gets supplier detail", async () => {
    const res = await app.inject({
      method: "GET",
      url: `/api/admin/warehouse/suppliers/${createdSupplierId}`,
      headers: authHeader(adminToken),
    });
    expect(res.statusCode).toBe(200);
    const body = res.json();
    expect(body.success).toBe(true);
    expect(body.data.name).toBe(`${N}supplier1`);
  });

  it("updates a supplier", async () => {
    const res = await app.inject({
      method: "PUT",
      url: `/api/admin/warehouse/suppliers/${createdSupplierId}`,
      headers: authHeader(adminToken),
      payload: { name: `${N}supplier1_updated` },
    });
    expect(res.statusCode).toBe(200);
    const body = res.json();
    expect(body.success).toBe(true);
    expect(body.data.name).toBe(`${N}supplier1_updated`);
  });

  it("soft-deletes a supplier (sets is_active=false)", async () => {
    const res = await app.inject({
      method: "DELETE",
      url: `/api/admin/warehouse/suppliers/${createdSupplierId}`,
      headers: authHeader(adminToken),
    });
    expect(res.statusCode).toBe(200);
    const body = res.json();
    expect(body.success).toBe(true);

    // Verify is_active is now false
    const checkRes = await app.inject({
      method: "GET",
      url: `/api/admin/warehouse/suppliers/${createdSupplierId}`,
      headers: authHeader(adminToken),
    });
    expect(checkRes.json().data.is_active).toBe(false);
  });

  it("returns 404 for nonexistent supplier", async () => {
    const res = await app.inject({
      method: "GET",
      url: "/api/admin/warehouse/suppliers/999999",
      headers: authHeader(adminToken),
    });
    expect(res.statusCode).toBe(404);
  });
});

// ---------------------------------------------------------------------------
// 3. Location CRUD
// ---------------------------------------------------------------------------

describe("Location CRUD", () => {
  let createdLocationId: number;
  let createdLocationCode: string;

  it("creates a location", async () => {
    createdLocationCode = uniqueCode();
    const res = await app.inject({
      method: "POST",
      url: "/api/admin/warehouse/locations",
      headers: authHeader(adminToken),
      payload: { code: createdLocationCode, name: `${N}location1` },
    });
    expect(res.statusCode).toBe(201);
    const body = res.json();
    expect(body.success).toBe(true);
    expect(body.data.code).toBe(createdLocationCode);
    createdLocationId = body.data.id;
  });

  it("rejects duplicate location code", async () => {
    const res = await app.inject({
      method: "POST",
      url: "/api/admin/warehouse/locations",
      headers: authHeader(adminToken),
      payload: { code: createdLocationCode, name: "duplicate" },
    });
    expect(res.statusCode).toBe(409);
  });

  it("lists locations (only active)", async () => {
    const res = await app.inject({
      method: "GET",
      url: "/api/admin/warehouse/locations",
      headers: authHeader(adminToken),
    });
    expect(res.statusCode).toBe(200);
    const body = res.json();
    expect(body.success).toBe(true);
    expect(Array.isArray(body.data)).toBe(true);
    const found = body.data.find(
      (l: { id: number }) => l.id === createdLocationId,
    );
    expect(found).toBeDefined();
  });

  it("updates a location", async () => {
    const res = await app.inject({
      method: "PUT",
      url: `/api/admin/warehouse/locations/${createdLocationId}`,
      headers: authHeader(adminToken),
      payload: { name: `${N}location1_updated` },
    });
    expect(res.statusCode).toBe(200);
    const body = res.json();
    expect(body.success).toBe(true);
    expect(body.data.name).toBe(`${N}location1_updated`);
  });

  it("deletes a location with no items at it", async () => {
    // First create a fresh location with no items
    const createRes = await app.inject({
      method: "POST",
      url: "/api/admin/warehouse/locations",
      headers: authHeader(adminToken),
      payload: { code: uniqueCode(), name: "to delete" },
    });
    const locId = createRes.json().data.id;

    const delRes = await app.inject({
      method: "DELETE",
      url: `/api/admin/warehouse/locations/${locId}`,
      headers: authHeader(adminToken),
    });
    expect(delRes.statusCode).toBe(200);
  });

  it("blocks delete if items with non-zero quantity are at the location", async () => {
    // Create a location, item, and a confirmed receipt to put stock at the location
    const locRes = await app.inject({
      method: "POST",
      url: "/api/admin/warehouse/locations",
      headers: authHeader(adminToken),
      payload: { code: uniqueCode(), name: "blocked loc" },
    });
    const blockedLocId = locRes.json().data.id;

    const itemRes = await app.inject({
      method: "POST",
      url: "/api/admin/warehouse/items",
      headers: authHeader(adminToken),
      payload: { name: `${N}item_loc_block`, unit: "ks" },
    });
    const blockItemId = itemRes.json().data.id;

    // Create and confirm a receipt to put stock at the location
    const receiptRes = await app.inject({
      method: "POST",
      url: "/api/admin/warehouse/receipts",
      headers: authHeader(adminToken),
      payload: {
        notes: `${N}receipt_for_loc_block`,
        items: [
          {
            item_id: blockItemId,
            quantity: 10,
            unit_price: 100,
            location_id: blockedLocId,
          },
        ],
      },
    });
    const receiptId = receiptRes.json().data.id;

    await app.inject({
      method: "POST",
      url: `/api/admin/warehouse/receipts/${receiptId}/confirm`,
      headers: authHeader(adminToken),
    });

    // Now try to delete the location — should be blocked
    const delRes = await app.inject({
      method: "DELETE",
      url: `/api/admin/warehouse/locations/${blockedLocId}`,
      headers: authHeader(adminToken),
    });
    expect(delRes.statusCode).toBe(409);
  });
});

// ---------------------------------------------------------------------------
// 4. Item CRUD
// ---------------------------------------------------------------------------

describe("Item CRUD", () => {
  let createdItemId: number;

  it("creates an item", async () => {
    const res = await app.inject({
      method: "POST",
      url: "/api/admin/warehouse/items",
      headers: authHeader(adminToken),
      payload: {
        name: `${N}item1`,
        unit: "ks",
        item_number: `ITM001`,
      },
    });
    expect(res.statusCode).toBe(201);
    const body = res.json();
    expect(body.success).toBe(true);
    expect(body.data.name).toBe(`${N}item1`);
    expect(body.data.unit).toBe("ks");
    expect(body.data.is_active).toBe(true);
    createdItemId = body.data.id;
  });

  it("lists items", async () => {
    const res = await app.inject({
      method: "GET",
      url: "/api/admin/warehouse/items",
      headers: authHeader(adminToken),
    });
    expect(res.statusCode).toBe(200);
    const body = res.json();
    expect(body.success).toBe(true);
    expect(Array.isArray(body.data)).toBe(true);
  });

  it("gets item detail", async () => {
    const res = await app.inject({
      method: "GET",
      url: `/api/admin/warehouse/items/${createdItemId}`,
      headers: authHeader(adminToken),
    });
    expect(res.statusCode).toBe(200);
    const body = res.json();
    expect(body.success).toBe(true);
    expect(body.data.name).toBe(`${N}item1`);
  });

  it("updates an item", async () => {
    const res = await app.inject({
      method: "PUT",
      url: `/api/admin/warehouse/items/${createdItemId}`,
      headers: authHeader(adminToken),
      payload: { name: `${N}item1_updated` },
    });
    expect(res.statusCode).toBe(200);
    const body = res.json();
    expect(body.success).toBe(true);
    expect(body.data.name).toBe(`${N}item1_updated`);
  });

  it("hard-deletes an item with no stock (row is gone, 404 afterwards)", async () => {
    // The current API HARD-deletes the item — the route does
    // `prisma.sklad_items.delete`, NOT an `is_active=false` soft toggle. The
    // test name reflects that real behavior so a future intended switch to
    // soft-delete would (correctly) make this test fail and prompt a review,
    // rather than the old "soft delete" name masking the mismatch.
    const res = await app.inject({
      method: "DELETE",
      url: `/api/admin/warehouse/items/${createdItemId}`,
      headers: authHeader(adminToken),
    });
    expect(res.statusCode).toBe(200);
    const body = res.json();
    expect(body.success).toBe(true);

    // The row is actually gone — a subsequent GET 404s.
    const checkRes = await app.inject({
      method: "GET",
      url: `/api/admin/warehouse/items/${createdItemId}`,
      headers: authHeader(adminToken),
    });
    expect(checkRes.statusCode).toBe(404);
  });

  it("blocks unit change when batches exist", async () => {
    // Create a fresh item
    const itemRes = await app.inject({
      method: "POST",
      url: "/api/admin/warehouse/items",
      headers: authHeader(adminToken),
      payload: {
        name: `${N}item_unit_change`,
        unit: "ks",
      },
    });
    const unitChangeItemId = itemRes.json().data.id;

    // Create a location
    const locRes = await app.inject({
      method: "POST",
      url: "/api/admin/warehouse/locations",
      headers: authHeader(adminToken),
      payload: { code: uniqueCode(), name: "unit change loc" },
    });
    const unitChangeLocId = locRes.json().data.id;

    // Create and confirm a receipt to create a batch
    const receiptRes = await app.inject({
      method: "POST",
      url: "/api/admin/warehouse/receipts",
      headers: authHeader(adminToken),
      payload: {
        notes: `${N}receipt_for_unit_change`,
        items: [
          {
            item_id: unitChangeItemId,
            quantity: 5,
            unit_price: 50,
            location_id: unitChangeLocId,
          },
        ],
      },
    });
    const receiptId = receiptRes.json().data.id;

    await app.inject({
      method: "POST",
      url: `/api/admin/warehouse/receipts/${receiptId}/confirm`,
      headers: authHeader(adminToken),
    });

    // Try to change the unit — should be blocked
    const updateRes = await app.inject({
      method: "PUT",
      url: `/api/admin/warehouse/items/${unitChangeItemId}`,
      headers: authHeader(adminToken),
      payload: { unit: "m" },
    });
    expect(updateRes.statusCode).toBe(409);
  });
});

// ---------------------------------------------------------------------------
// 5. Receipt flow
// ---------------------------------------------------------------------------

describe("Receipt flow", () => {
  let receiptItemId: number;
  let receiptLocId: number;

  beforeEach(async () => {
    // Create a fresh item and location for each test
    const itemRes = await app.inject({
      method: "POST",
      url: "/api/admin/warehouse/items",
      headers: authHeader(adminToken),
      payload: { name: `${N}item_receipt_flow`, unit: "ks" },
    });
    receiptItemId = itemRes.json().data.id;

    const locRes = await app.inject({
      method: "POST",
      url: "/api/admin/warehouse/locations",
      headers: authHeader(adminToken),
      payload: { code: uniqueCode(), name: "receipt flow loc" },
    });
    receiptLocId = locRes.json().data.id;
  });

  it("creates receipt in DRAFT, confirms, then cancels (storno)", async () => {
    // 1. Create receipt in DRAFT
    const createRes = await app.inject({
      method: "POST",
      url: "/api/admin/warehouse/receipts",
      headers: authHeader(adminToken),
      payload: {
        notes: `${N}receipt_flow_test`,
        items: [
          {
            item_id: receiptItemId,
            quantity: 20,
            unit_price: 150,
            location_id: receiptLocId,
          },
        ],
      },
    });
    expect(createRes.statusCode).toBe(201);
    const receiptId = createRes.json().data.id;
    expect(createRes.json().data.status).toBe("DRAFT");

    // 2. Confirm receipt
    const confirmRes = await app.inject({
      method: "POST",
      url: `/api/admin/warehouse/receipts/${receiptId}/confirm`,
      headers: authHeader(adminToken),
    });
    expect(confirmRes.statusCode).toBe(200);
    expect(confirmRes.json().data.receipt_number).toBeTruthy();

    // 3. Verify batches were created
    const batchesRes = await app.inject({
      method: "GET",
      url: `/api/admin/warehouse/items/${receiptItemId}/batches`,
      headers: authHeader(adminToken),
    });
    expect(batchesRes.statusCode).toBe(200);
    const batches = batchesRes.json().data.batches;
    expect(Array.isArray(batches)).toBe(true);
    expect(batches.length).toBeGreaterThanOrEqual(1);
    expect(Number(batches[0].quantity)).toBe(20);

    // 4. Verify item_locations updated
    const itemDetailRes = await app.inject({
      method: "GET",
      url: `/api/admin/warehouse/items/${receiptItemId}`,
      headers: authHeader(adminToken),
    });
    const itemLocations = itemDetailRes.json().data.item_locations;
    const loc = itemLocations.find(
      (il: { location_id: number }) => il.location_id === receiptLocId,
    );
    expect(loc).toBeDefined();
    expect(Number(loc.quantity)).toBe(20);

    // 5. Cancel receipt (storno)
    const cancelRes = await app.inject({
      method: "POST",
      url: `/api/admin/warehouse/receipts/${receiptId}/cancel`,
      headers: authHeader(adminToken),
    });
    expect(cancelRes.statusCode).toBe(200);

    // 6. Verify reversed — check item available qty
    const availRes = await app.inject({
      method: "GET",
      url: `/api/admin/warehouse/items/${receiptItemId}/available-qty`,
      headers: authHeader(adminToken),
    });
    expect(availRes.json().data.available_quantity).toBe(0);
  });

  it("rejects confirming a non-DRAFT receipt", async () => {
    const createRes = await app.inject({
      method: "POST",
      url: "/api/admin/warehouse/receipts",
      headers: authHeader(adminToken),
      payload: {
        notes: `${N}receipt_double_confirm`,
        items: [
          {
            item_id: receiptItemId,
            quantity: 5,
            unit_price: 10,
            location_id: receiptLocId,
          },
        ],
      },
    });
    const receiptId = createRes.json().data.id;

    // Confirm once
    await app.inject({
      method: "POST",
      url: `/api/admin/warehouse/receipts/${receiptId}/confirm`,
      headers: authHeader(adminToken),
    });

    // Try to confirm again
    const secondConfirm = await app.inject({
      method: "POST",
      url: `/api/admin/warehouse/receipts/${receiptId}/confirm`,
      headers: authHeader(adminToken),
    });
    expect(secondConfirm.statusCode).toBe(400);
  });

  it("cancels a DRAFT receipt (no stock reversal needed)", async () => {
    const createRes = await app.inject({
      method: "POST",
      url: "/api/admin/warehouse/receipts",
      headers: authHeader(adminToken),
      payload: {
        notes: `${N}receipt_cancel_draft`,
        items: [
          {
            item_id: receiptItemId,
            quantity: 3,
            unit_price: 10,
          },
        ],
      },
    });
    const receiptId = createRes.json().data.id;

    const cancelRes = await app.inject({
      method: "POST",
      url: `/api/admin/warehouse/receipts/${receiptId}/cancel`,
      headers: authHeader(adminToken),
    });
    expect(cancelRes.statusCode).toBe(200);

    // Verify receipt is CANCELLED
    const detailRes = await app.inject({
      method: "GET",
      url: `/api/admin/warehouse/receipts/${receiptId}`,
      headers: authHeader(adminToken),
    });
    expect(detailRes.json().data.status).toBe("CANCELLED");
  });
});

// ---------------------------------------------------------------------------
// 5b. Receipt attachment download
// ---------------------------------------------------------------------------

describe("Receipt attachment download", () => {
  let receiptId: number;
  let otherReceiptId: number;
  let attachmentId: number;
  let diacriticAttachmentId: number;
  const fileBytes = Buffer.from("%PDF-1.4 fake attachment bytes", "utf8");
  const filePath = "Přijaté/2026/06/wh_test_priloha.pdf";
  const diacriticFileName = "příloha č. 1.pdf";

  beforeAll(async () => {
    // Item + two receipts (attachment belongs to the first one only)
    const itemRes = await app.inject({
      method: "POST",
      url: "/api/admin/warehouse/items",
      headers: authHeader(adminToken),
      payload: { name: `${N}item_att_download`, unit: "ks" },
    });
    const itemId = itemRes.json().data.id;

    const receiptRes = await app.inject({
      method: "POST",
      url: "/api/admin/warehouse/receipts",
      headers: authHeader(adminToken),
      payload: {
        notes: `${N}att_download_a`,
        items: [{ item_id: itemId, quantity: 1, unit_price: 10 }],
      },
    });
    receiptId = receiptRes.json().data.id;

    const otherReceiptRes = await app.inject({
      method: "POST",
      url: "/api/admin/warehouse/receipts",
      headers: authHeader(adminToken),
      payload: {
        notes: `${N}att_download_b`,
        items: [{ item_id: itemId, quantity: 1, unit_price: 10 }],
      },
    });
    otherReceiptId = otherReceiptRes.json().data.id;

    // Attachment row created directly — the upload endpoint needs a writable
    // NAS, which is not available in the test environment. The NAS read is
    // stubbed per-test on the singleton (same temp-dir/stub seam as the
    // nas-document-manager tests); the DB rows and route logic stay real.
    const attachment = await prisma.sklad_receipt_attachments.create({
      data: {
        receipt_id: receiptId,
        file_name: "wh_test_priloha.pdf",
        file_mime: "application/pdf",
        file_size: fileBytes.length,
        file_path: filePath,
      },
    });
    attachmentId = attachment.id;

    // Diacritic filename — the norm for a Czech company. Node throws on
    // header values with chars > U+00FF, so this exercises the RFC 5987 path.
    const diacriticAttachment = await prisma.sklad_receipt_attachments.create({
      data: {
        receipt_id: receiptId,
        file_name: diacriticFileName,
        file_mime: "application/pdf",
        file_size: fileBytes.length,
        file_path: "Přijaté/2026/06/wh_test_priloha_diakritika.pdf",
      },
    });
    diacriticAttachmentId = diacriticAttachment.id;
  });

  afterEach(() => {
    vi.restoreAllMocks();
  });

  it("downloads an existing attachment with correct content-type and bytes", async () => {
    const readSpy = vi
      .spyOn(nasInvoicesManager, "readReceived")
      .mockReturnValue({ data: fileBytes, fileName: "wh_test_priloha.pdf" });

    const res = await app.inject({
      method: "GET",
      url: `/api/admin/warehouse/receipts/${receiptId}/attachments/${attachmentId}`,
      headers: authHeader(adminToken),
    });

    expect(res.statusCode).toBe(200);
    expect(res.headers["content-type"]).toBe("application/pdf");
    expect(res.headers["content-disposition"]).toBe(
      `attachment; filename="wh_test_priloha.pdf"; filename*=UTF-8''wh_test_priloha.pdf`,
    );
    expect(res.rawPayload.equals(fileBytes)).toBe(true);
    expect(readSpy).toHaveBeenCalledWith(filePath);
  });

  it("downloads an attachment with a Czech diacritic filename (RFC 5987)", async () => {
    vi.spyOn(nasInvoicesManager, "readReceived").mockReturnValue({
      data: fileBytes,
      fileName: diacriticFileName,
    });

    const res = await app.inject({
      method: "GET",
      url: `/api/admin/warehouse/receipts/${receiptId}/attachments/${diacriticAttachmentId}`,
      headers: authHeader(adminToken),
    });

    expect(res.statusCode).toBe(200);
    const disposition = res.headers["content-disposition"];
    // The real UTF-8 name travels percent-encoded in filename* (RFC 5987) …
    expect(disposition).toContain(
      `filename*=UTF-8''${encodeURIComponent(diacriticFileName)}`,
    );
    // … alongside an ASCII-only fallback, so the header never carries
    // chars > U+00FF (which Node's setHeader rejects → 500).
    expect(disposition).toBe(
      `attachment; filename="p__loha _. 1.pdf"; filename*=UTF-8''p%C5%99%C3%ADloha%20%C4%8D.%201.pdf`,
    );
    expect(res.rawPayload.equals(fileBytes)).toBe(true);
  });

  it("returns 404 when the attachment belongs to a different receipt", async () => {
    const readSpy = vi.spyOn(nasInvoicesManager, "readReceived");

    const res = await app.inject({
      method: "GET",
      url: `/api/admin/warehouse/receipts/${otherReceiptId}/attachments/${attachmentId}`,
      headers: authHeader(adminToken),
    });

    expect(res.statusCode).toBe(404);
    expect(res.json().success).toBe(false);
    // Ownership is rejected before any NAS access
    expect(readSpy).not.toHaveBeenCalled();
  });

  it("returns 404 for a nonexistent attachment id", async () => {
    const res = await app.inject({
      method: "GET",
      url: `/api/admin/warehouse/receipts/${receiptId}/attachments/99999999`,
      headers: authHeader(adminToken),
    });
    expect(res.statusCode).toBe(404);
    expect(res.json().success).toBe(false);
  });

  it("returns 404 when the file is missing on the NAS", async () => {
    vi.spyOn(nasInvoicesManager, "readReceived").mockReturnValue(null);

    const res = await app.inject({
      method: "GET",
      url: `/api/admin/warehouse/receipts/${receiptId}/attachments/${attachmentId}`,
      headers: authHeader(adminToken),
    });
    expect(res.statusCode).toBe(404);
    expect(res.json().success).toBe(false);
  });

  it("rejects download without warehouse.view permission (403)", async () => {
    const res = await app.inject({
      method: "GET",
      url: `/api/admin/warehouse/receipts/${receiptId}/attachments/${attachmentId}`,
      headers: authHeader(noPermToken),
    });
    expect(res.statusCode).toBe(403);
  });
});

// ---------------------------------------------------------------------------
// 6. Issue flow
// ---------------------------------------------------------------------------

describe("Issue flow", () => {
  let issueItemId: number;
  let issueLocId: number;
  let issueReceiptId: number;

  beforeEach(async () => {
    // Create item + location, then confirm a receipt to have stock
    const itemRes = await app.inject({
      method: "POST",
      url: "/api/admin/warehouse/items",
      headers: authHeader(adminToken),
      payload: { name: `${N}item_issue_flow`, unit: "ks" },
    });
    issueItemId = itemRes.json().data.id;

    const locRes = await app.inject({
      method: "POST",
      url: "/api/admin/warehouse/locations",
      headers: authHeader(adminToken),
      payload: { code: uniqueCode(), name: "issue flow loc" },
    });
    issueLocId = locRes.json().data.id;

    // Create and confirm receipt to have stock
    const receiptRes = await app.inject({
      method: "POST",
      url: "/api/admin/warehouse/receipts",
      headers: authHeader(adminToken),
      payload: {
        notes: `${N}receipt_for_issue`,
        items: [
          {
            item_id: issueItemId,
            quantity: 50,
            unit_price: 100,
            location_id: issueLocId,
          },
        ],
      },
    });
    issueReceiptId = receiptRes.json().data.id;

    await app.inject({
      method: "POST",
      url: `/api/admin/warehouse/receipts/${issueReceiptId}/confirm`,
      headers: authHeader(adminToken),
    });
  });

  it("creates issue, confirms, then cancels — batches decremented then restored", async () => {
    // 1. Create issue (DRAFT)
    const createRes = await app.inject({
      method: "POST",
      url: "/api/admin/warehouse/issues",
      headers: authHeader(adminToken),
      payload: {
        project_id: projectId,
        notes: `${N}issue_flow_test`,
        items: [
          {
            item_id: issueItemId,
            quantity: 10,
            location_id: issueLocId,
          },
        ],
      },
    });
    expect(createRes.statusCode).toBe(201);
    const issueId = createRes.json().data.id;
    expect(createRes.json().data.status).toBe("DRAFT");

    // 2. Confirm issue
    const confirmRes = await app.inject({
      method: "POST",
      url: `/api/admin/warehouse/issues/${issueId}/confirm`,
      headers: authHeader(adminToken),
    });
    expect(confirmRes.statusCode).toBe(200);
    expect(confirmRes.json().data.issue_number).toBeTruthy();

    // 3. Verify batches decremented
    const batchesRes = await app.inject({
      method: "GET",
      url: `/api/admin/warehouse/items/${issueItemId}/batches`,
      headers: authHeader(adminToken),
    });
    const batches = batchesRes.json().data.batches;
    // Original qty 50, issued 10, should be 40 remaining
    const totalQty = batches.reduce(
      (sum: number, b: { quantity: unknown }) => sum + Number(b.quantity),
      0,
    );
    expect(totalQty).toBe(40);

    // 4. Verify item_locations decremented
    const itemDetailRes = await app.inject({
      method: "GET",
      url: `/api/admin/warehouse/items/${issueItemId}`,
      headers: authHeader(adminToken),
    });
    const loc = itemDetailRes
      .json()
      .data.item_locations.find(
        (il: { location_id: number }) => il.location_id === issueLocId,
      );
    expect(Number(loc.quantity)).toBe(40);

    // 5. Cancel issue
    const cancelRes = await app.inject({
      method: "POST",
      url: `/api/admin/warehouse/issues/${issueId}/cancel`,
      headers: authHeader(adminToken),
    });
    expect(cancelRes.statusCode).toBe(200);

    // 6. Verify batches restored
    const batchesAfterRes = await app.inject({
      method: "GET",
      url: `/api/admin/warehouse/items/${issueItemId}/batches`,
      headers: authHeader(adminToken),
    });
    const batchesAfter = batchesAfterRes.json().data.batches;
    const totalQtyAfter = batchesAfter.reduce(
      (sum: number, b: { quantity: unknown }) => sum + Number(b.quantity),
      0,
    );
    expect(totalQtyAfter).toBe(50);
  });

  it("rejects issue when insufficient stock", async () => {
    const createRes = await app.inject({
      method: "POST",
      url: "/api/admin/warehouse/issues",
      headers: authHeader(adminToken),
      payload: {
        project_id: projectId,
        notes: `${N}issue_insufficient`,
        items: [
          {
            item_id: issueItemId,
            quantity: 999,
          },
        ],
      },
    });
    // Auto-FIFO resolution should fail at creation time
    expect(createRes.statusCode).toBe(400);
  });
});

// ---------------------------------------------------------------------------
// 6b. FIFO ordering — the OLDEST batch is consumed first
// ---------------------------------------------------------------------------

describe("Issue FIFO ordering (oldest batch first)", () => {
  it("consumes the batch with the earliest received_at first, not the lowest id", async () => {
    // Build two batches for one item via two confirmed receipts.
    const itemRes = await app.inject({
      method: "POST",
      url: "/api/admin/warehouse/items",
      headers: authHeader(adminToken),
      payload: { name: `${N}item_fifo`, unit: "ks" },
    });
    const fifoItemId = itemRes.json().data.id as number;

    const locA = (
      await app.inject({
        method: "POST",
        url: "/api/admin/warehouse/locations",
        headers: authHeader(adminToken),
        payload: { code: uniqueCode(), name: "fifo loc A" },
      })
    ).json().data.id as number;
    const locB = (
      await app.inject({
        method: "POST",
        url: "/api/admin/warehouse/locations",
        headers: authHeader(adminToken),
        payload: { code: uniqueCode(), name: "fifo loc B" },
      })
    ).json().data.id as number;

    // Helper: create + confirm a receipt of `qty` at `locId`.
    const confirmReceipt = async (qty: number, locId: number) => {
      const r = await app.inject({
        method: "POST",
        url: "/api/admin/warehouse/receipts",
        headers: authHeader(adminToken),
        payload: {
          notes: `${N}fifo_receipt`,
          items: [
            {
              item_id: fifoItemId,
              quantity: qty,
              unit_price: 10,
              location_id: locId,
            },
          ],
        },
      });
      const id = r.json().data.id as number;
      await app.inject({
        method: "POST",
        url: `/api/admin/warehouse/receipts/${id}/confirm`,
        headers: authHeader(adminToken),
      });
    };

    // batchEarly is created FIRST (lower id); batchLate is created SECOND
    // (higher id). We then flip their received_at so the HIGHER-id batch is the
    // OLDEST — this is what lets the test prove FIFO sorts by received_at, not
    // by insertion id.
    await confirmReceipt(10, locA); // → batch with the lower id
    await confirmReceipt(10, locB); // → batch with the higher id

    const batchesRaw = await prisma.sklad_batches.findMany({
      where: { item_id: fifoItemId },
      orderBy: { id: "asc" },
    });
    expect(batchesRaw.length).toBe(2);
    const lowerId = batchesRaw[0]; // created first
    const higherId = batchesRaw[1]; // created second

    // Make the HIGHER-id batch the OLDEST by received_at.
    await prisma.sklad_batches.update({
      where: { id: lowerId.id },
      data: { received_at: new Date("2021-01-01T00:00:00Z") },
    });
    await prisma.sklad_batches.update({
      where: { id: higherId.id },
      data: { received_at: new Date("2020-01-01T00:00:00Z") }, // older
    });

    // Issue 5 with no explicit batch → FIFO must auto-select the OLDEST batch
    // (the higher-id one we back-dated), partially depleting it.
    const issueRes = await app.inject({
      method: "POST",
      url: "/api/admin/warehouse/issues",
      headers: authHeader(adminToken),
      payload: {
        project_id: projectId,
        notes: `${N}fifo_issue`,
        items: [{ item_id: fifoItemId, quantity: 5 }],
      },
    });
    expect(issueRes.statusCode).toBe(201);
    const issueId = issueRes.json().data.id as number;
    const confirmRes = await app.inject({
      method: "POST",
      url: `/api/admin/warehouse/issues/${issueId}/confirm`,
      headers: authHeader(adminToken),
    });
    expect(confirmRes.statusCode).toBe(200);

    // The OLDEST batch (higher id, received 2020) is depleted from 10 → 5;
    // the newer batch (lower id, received 2021) is untouched at 10. If FIFO
    // wrongly sorted by id it would have consumed the lower-id batch instead.
    const oldest = await prisma.sklad_batches.findUnique({
      where: { id: higherId.id },
    });
    const newest = await prisma.sklad_batches.findUnique({
      where: { id: lowerId.id },
    });
    expect(Number(oldest!.quantity)).toBe(5);
    expect(Number(newest!.quantity)).toBe(10);
  });
});

// ---------------------------------------------------------------------------
// 7. Reservation flow
// ---------------------------------------------------------------------------

describe("Reservation flow", () => {
  let resItemId: number;
  let resLocId: number;

  beforeEach(async () => {
    const itemRes = await app.inject({
      method: "POST",
      url: "/api/admin/warehouse/items",
      headers: authHeader(adminToken),
      payload: { name: `${N}item_reservation`, unit: "ks" },
    });
    resItemId = itemRes.json().data.id;

    const locRes = await app.inject({
      method: "POST",
      url: "/api/admin/warehouse/locations",
      headers: authHeader(adminToken),
      payload: { code: uniqueCode(), name: "reservation loc" },
    });
    resLocId = locRes.json().data.id;

    // Create and confirm receipt to have stock
    const receiptRes = await app.inject({
      method: "POST",
      url: "/api/admin/warehouse/receipts",
      headers: authHeader(adminToken),
      payload: {
        notes: `${N}receipt_for_reservation`,
        items: [
          {
            item_id: resItemId,
            quantity: 100,
            unit_price: 50,
            location_id: resLocId,
          },
        ],
      },
    });
    await app.inject({
      method: "POST",
      url: `/api/admin/warehouse/receipts/${receiptRes.json().data.id}/confirm`,
      headers: authHeader(adminToken),
    });
  });

  it("creates reservation — available_qty decreases, cancel restores it", async () => {
    // Check initial available qty
    const beforeRes = await app.inject({
      method: "GET",
      url: `/api/admin/warehouse/items/${resItemId}/available-qty`,
      headers: authHeader(adminToken),
    });
    const beforeQty = beforeRes.json().data.available_quantity;
    expect(beforeQty).toBe(100);

    // Create reservation
    const createRes = await app.inject({
      method: "POST",
      url: "/api/admin/warehouse/reservations",
      headers: authHeader(adminToken),
      payload: {
        item_id: resItemId,
        project_id: projectId,
        quantity: 30,
        notes: `${N}reservation_test`,
      },
    });
    expect(createRes.statusCode).toBe(201);
    expect(createRes.json().data.status).toBe("ACTIVE");
    const reservationId = createRes.json().data.id;

    // Verify available_qty decreased
    const afterCreateRes = await app.inject({
      method: "GET",
      url: `/api/admin/warehouse/items/${resItemId}/available-qty`,
      headers: authHeader(adminToken),
    });
    expect(afterCreateRes.json().data.available_quantity).toBe(70);

    // Cancel reservation
    const cancelRes = await app.inject({
      method: "POST",
      url: `/api/admin/warehouse/reservations/${reservationId}/cancel`,
      headers: authHeader(adminToken),
    });
    expect(cancelRes.statusCode).toBe(200);

    // Verify available_qty restored
    const afterCancelRes = await app.inject({
      method: "GET",
      url: `/api/admin/warehouse/items/${resItemId}/available-qty`,
      headers: authHeader(adminToken),
    });
    expect(afterCancelRes.json().data.available_quantity).toBe(100);
  });

  it("rejects reservation when available quantity is insufficient", async () => {
    const createRes = await app.inject({
      method: "POST",
      url: "/api/admin/warehouse/reservations",
      headers: authHeader(adminToken),
      payload: {
        item_id: resItemId,
        project_id: projectId,
        quantity: 999,
        notes: `${N}reservation_insufficient`,
      },
    });
    expect(createRes.statusCode).toBe(400);
  });

  it("rejects cancelling an already cancelled reservation", async () => {
    const createRes = await app.inject({
      method: "POST",
      url: "/api/admin/warehouse/reservations",
      headers: authHeader(adminToken),
      payload: {
        item_id: resItemId,
        project_id: projectId,
        quantity: 5,
        notes: `${N}reservation_double_cancel`,
      },
    });
    const reservationId = createRes.json().data.id;

    // Cancel once
    await app.inject({
      method: "POST",
      url: `/api/admin/warehouse/reservations/${reservationId}/cancel`,
      headers: authHeader(adminToken),
    });

    // Try to cancel again
    const secondCancel = await app.inject({
      method: "POST",
      url: `/api/admin/warehouse/reservations/${reservationId}/cancel`,
      headers: authHeader(adminToken),
    });
    expect(secondCancel.statusCode).toBe(400);
  });
});

// ---------------------------------------------------------------------------
// 8. Permission checks
// ---------------------------------------------------------------------------

describe("Permission checks", () => {
  it("rejects requests without authentication (401)", async () => {
    const res = await app.inject({
      method: "GET",
      url: "/api/admin/warehouse/categories",
    });
    expect(res.statusCode).toBe(401);
  });

  it("rejects warehouse.manage endpoints without permission (403)", async () => {
    const endpoints = [
      { method: "GET", url: "/api/admin/warehouse/categories" },
      { method: "POST", url: "/api/admin/warehouse/categories" },
      { method: "GET", url: "/api/admin/warehouse/suppliers" },
      { method: "GET", url: "/api/admin/warehouse/locations" },
      { method: "POST", url: "/api/admin/warehouse/items" },
    ];

    for (const ep of endpoints) {
      const res = await app.inject({
        method: ep.method as "GET" | "POST",
        url: ep.url,
        headers: authHeader(noPermToken),
        payload:
          ep.method === "POST"
            ? { name: "x", code: "x", unit: "ks" }
            : undefined,
      });
      expect(res.statusCode).toBe(403);
    }
  });

  it("rejects warehouse.view endpoints without permission (403)", async () => {
    const res = await app.inject({
      method: "GET",
      url: "/api/admin/warehouse/items",
      headers: authHeader(noPermToken),
    });
    expect(res.statusCode).toBe(403);
  });

  it("rejects warehouse.operate endpoints without permission (403)", async () => {
    const res = await app.inject({
      method: "POST",
      url: "/api/admin/warehouse/receipts",
      headers: authHeader(noPermToken),
      payload: {
        items: [{ item_id: 1, quantity: 1, unit_price: 1 }],
      },
    });
    expect(res.statusCode).toBe(403);
  });

  it("rejects warehouse.inventory endpoints without permission (403)", async () => {
    const res = await app.inject({
      method: "GET",
      url: "/api/admin/warehouse/inventory-sessions",
      headers: authHeader(noPermToken),
    });
    expect(res.statusCode).toBe(403);
  });
});
