import { DispoStagedRecordType, ImportBatchStatus, StagedRecordStatus } from "@capakraken/db";
import { TRPCError } from "@trpc/server";
import { beforeEach, describe, expect, it, vi } from "vitest";

vi.mock("../lib/audit.js", () => ({
  createAuditEntry: vi.fn(),
}));

vi.mock("@capakraken/application", () => ({
  commitDispoImportBatch: vi.fn().mockResolvedValue({ resources: 5, projects: 3 }),
}));

import { createAuditEntry } from "../lib/audit.js";
import { commitDispoImportBatch } from "@capakraken/application";
import {
  assertImportBatchCancelable,
  buildCancelledImportBatchUpdateData,
  buildDispoImportCommitAuditSummary,
  buildResolvedStagedRecordUpdateData,
  resolveDispoStagedRecordStatus,
  resolveDispoStagedRecordStoreKey,
} from "../router/dispo-management-support.js";
import {
  cancelImportBatch,
  commitImportBatch,
  resolveStagedRecord,
} from "../router/dispo-management.js";
import {
  getImportBatch,
  listImportBatches,
  listStagedAssignments,
  listStagedResources,
  listStagedUnresolvedRecords,
} from "../router/dispo-read.js";

// ─── dispo-management-support pure functions ──────────────────────────────────

describe("assertImportBatchCancelable", () => {
  it("does not throw for non-terminal statuses", () => {
    expect(() =>
      assertImportBatchCancelable({ id: "batch_1", status: ImportBatchStatus.STAGED }),
    ).not.toThrow();

    expect(() =>
      assertImportBatchCancelable({ id: "batch_1", status: ImportBatchStatus.REVIEW_READY }),
    ).not.toThrow();
  });

  it("throws BAD_REQUEST for COMMITTED status", () => {
    expect(() =>
      assertImportBatchCancelable({ id: "batch_1", status: ImportBatchStatus.COMMITTED }),
    ).toThrow(
      new TRPCError({
        code: "BAD_REQUEST",
        message: 'Cannot cancel batch in status "COMMITTED"',
      }),
    );
  });

  it("throws BAD_REQUEST for CANCELLED status", () => {
    expect(() =>
      assertImportBatchCancelable({ id: "batch_1", status: ImportBatchStatus.CANCELLED }),
    ).toThrow(
      new TRPCError({
        code: "BAD_REQUEST",
        message: 'Cannot cancel batch in status "CANCELLED"',
      }),
    );
  });
});

describe("resolveDispoStagedRecordStatus", () => {
  it("maps APPROVE to APPROVED and REJECT/SKIP to REJECTED", () => {
    expect(resolveDispoStagedRecordStatus("APPROVE")).toBe(StagedRecordStatus.APPROVED);
    expect(resolveDispoStagedRecordStatus("REJECT")).toBe(StagedRecordStatus.REJECTED);
    expect(resolveDispoStagedRecordStatus("SKIP")).toBe(StagedRecordStatus.REJECTED);
  });
});

describe("resolveDispoStagedRecordStoreKey", () => {
  it("maps every DispoStagedRecordType enum value to the correct Prisma store key", () => {
    expect(resolveDispoStagedRecordStoreKey(DispoStagedRecordType.RESOURCE)).toBe("stagedResource");
    expect(resolveDispoStagedRecordStoreKey(DispoStagedRecordType.CLIENT)).toBe("stagedClient");
    expect(resolveDispoStagedRecordStoreKey(DispoStagedRecordType.PROJECT)).toBe("stagedProject");
    expect(resolveDispoStagedRecordStoreKey(DispoStagedRecordType.ASSIGNMENT)).toBe(
      "stagedAssignment",
    );
    expect(resolveDispoStagedRecordStoreKey(DispoStagedRecordType.VACATION)).toBe("stagedVacation");
    expect(resolveDispoStagedRecordStoreKey(DispoStagedRecordType.AVAILABILITY_RULE)).toBe(
      "stagedAvailabilityRule",
    );
    expect(resolveDispoStagedRecordStoreKey(DispoStagedRecordType.UNRESOLVED)).toBe(
      "stagedUnresolvedRecord",
    );
  });
});

describe("buildDispoImportCommitAuditSummary", () => {
  it("produces the expected summary string from counts", () => {
    expect(buildDispoImportCommitAuditSummary({ resources: 5, projects: 3 })).toBe(
      'Committed import batch ({"resources":5,"projects":3})',
    );
  });
});

describe("buildCancelledImportBatchUpdateData / buildResolvedStagedRecordUpdateData", () => {
  it("returns the correct update payloads", () => {
    expect(buildCancelledImportBatchUpdateData()).toEqual({
      status: ImportBatchStatus.CANCELLED,
    });

    expect(buildResolvedStagedRecordUpdateData("APPROVE")).toEqual({
      status: StagedRecordStatus.APPROVED,
    });

    expect(buildResolvedStagedRecordUpdateData("SKIP")).toEqual({
      status: StagedRecordStatus.REJECTED,
    });
  });
});

// ─── listImportBatches ────────────────────────────────────────────────────────

describe("listImportBatches", () => {
  it("returns paginated results and sets nextCursor when more items exist", async () => {
    const items = [
      { id: "batch_3" },
      { id: "batch_2" },
      { id: "batch_1" }, // extra item beyond limit
    ];
    const findMany = vi.fn().mockResolvedValue(items);
    const db = { importBatch: { findMany } };

    const result = await listImportBatches(db, { limit: 2 });

    expect(findMany).toHaveBeenCalledWith({
      where: {},
      orderBy: { createdAt: "desc" },
      take: 3,
    });
    expect(result.items).toHaveLength(2);
    expect(result.nextCursor).toBe("batch_1");
  });

  it("applies optional status filter to the where clause", async () => {
    const findMany = vi.fn().mockResolvedValue([]);
    const db = { importBatch: { findMany } };

    await listImportBatches(db, { limit: 10, status: ImportBatchStatus.STAGED });

    expect(findMany).toHaveBeenCalledWith(
      expect.objectContaining({
        where: { status: ImportBatchStatus.STAGED },
      }),
    );
  });
});

// ─── getImportBatch ───────────────────────────────────────────────────────────

describe("getImportBatch", () => {
  function buildCountDb(batchValue: unknown) {
    return {
      importBatch: { findUnique: vi.fn().mockResolvedValue(batchValue) },
      stagedResource: { count: vi.fn().mockResolvedValue(10) },
      stagedClient: { count: vi.fn().mockResolvedValue(2) },
      stagedProject: { count: vi.fn().mockResolvedValue(4) },
      stagedAssignment: { count: vi.fn().mockResolvedValue(20) },
      stagedVacation: { count: vi.fn().mockResolvedValue(1) },
      stagedAvailabilityRule: { count: vi.fn().mockResolvedValue(3) },
      stagedUnresolvedRecord: { count: vi.fn().mockResolvedValue(0) },
    };
  }

  it("returns the batch enriched with counts from all staged tables", async () => {
    const batch = { id: "batch_1", status: ImportBatchStatus.STAGED };
    const db = buildCountDb(batch);

    const result = await getImportBatch(db, "batch_1");

    expect(result).toMatchObject({
      id: "batch_1",
      counts: {
        resourceCount: 10,
        clientCount: 2,
        projectCount: 4,
        assignmentCount: 20,
        vacationCount: 1,
        availabilityRuleCount: 3,
        unresolvedCount: 0,
      },
    });
    // Each count query is scoped to the batch id
    expect(db.stagedResource.count).toHaveBeenCalledWith({ where: { importBatchId: "batch_1" } });
    expect(db.stagedUnresolvedRecord.count).toHaveBeenCalledWith({
      where: { importBatchId: "batch_1" },
    });
  });

  it("throws NOT_FOUND when the batch does not exist", async () => {
    const db = buildCountDb(null);

    await expect(getImportBatch(db, "missing_batch")).rejects.toMatchObject({
      code: "NOT_FOUND",
    });
    // Counts should not be fetched for a missing batch
    expect(db.stagedResource.count).not.toHaveBeenCalled();
  });
});

// ─── listStagedResources ──────────────────────────────────────────────────────

describe("listStagedResources", () => {
  it("paginates results using the cursor pattern", async () => {
    const findMany = vi.fn().mockResolvedValue([
      { id: "res_2" },
      { id: "res_1" }, // extra item that triggers cursor
    ]);
    const db = { stagedResource: { findMany } };

    const result = await listStagedResources(db, {
      importBatchId: "batch_1",
      limit: 1,
    });

    expect(findMany).toHaveBeenCalledWith(
      expect.objectContaining({
        where: { importBatchId: "batch_1" },
        take: 2,
      }),
    );
    expect(result.items).toHaveLength(1);
    expect(result.nextCursor).toBe("res_1");
  });

  it("passes cursor and status filter through to findMany", async () => {
    const findMany = vi.fn().mockResolvedValue([]);
    const db = { stagedResource: { findMany } };

    await listStagedResources(db, {
      importBatchId: "batch_1",
      limit: 25,
      cursor: "res_10",
      status: StagedRecordStatus.APPROVED,
    });

    expect(findMany).toHaveBeenCalledWith(
      expect.objectContaining({
        where: { importBatchId: "batch_1", status: StagedRecordStatus.APPROVED },
        cursor: { id: "res_10" },
        skip: 1,
      }),
    );
  });
});

// ─── cancelImportBatch ───────────────────────────────────────────────────────

describe("cancelImportBatch", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it("cancels a batch in a non-terminal status and creates an audit entry", async () => {
    const batch = { id: "batch_1", status: ImportBatchStatus.STAGED };
    const cancelled = { id: "batch_1", status: ImportBatchStatus.CANCELLED };
    const findUnique = vi.fn().mockResolvedValue(batch);
    const update = vi.fn().mockResolvedValue(cancelled);
    const db = { importBatch: { findUnique, update } };

    const result = await cancelImportBatch(db as never, { id: "batch_1", userId: "user_1" });

    expect(findUnique).toHaveBeenCalledWith({
      where: { id: "batch_1" },
      select: { id: true, status: true },
    });
    expect(update).toHaveBeenCalledWith({
      where: { id: "batch_1" },
      data: { status: ImportBatchStatus.CANCELLED },
    });
    expect(result).toEqual(cancelled);
    // Audit is fire-and-forget; verify it was initiated
    expect(createAuditEntry).toHaveBeenCalledWith(
      expect.objectContaining({
        entityType: "ImportBatch",
        entityId: "batch_1",
        action: "UPDATE",
        summary: "Cancelled import batch",
        userId: "user_1",
      }),
    );
  });

  it("throws NOT_FOUND when the batch does not exist", async () => {
    const findUnique = vi.fn().mockResolvedValue(null);
    const db = { importBatch: { findUnique, update: vi.fn() } };

    await expect(cancelImportBatch(db as never, { id: "missing_batch" })).rejects.toMatchObject({
      code: "NOT_FOUND",
    });

    expect(db.importBatch.update).not.toHaveBeenCalled();
  });

  it("throws BAD_REQUEST when the batch is already in a terminal status", async () => {
    const findUnique = vi.fn().mockResolvedValue({
      id: "batch_1",
      status: ImportBatchStatus.COMMITTED,
    });
    const update = vi.fn();
    const db = { importBatch: { findUnique, update } };

    await expect(cancelImportBatch(db as never, { id: "batch_1" })).rejects.toMatchObject({
      code: "BAD_REQUEST",
    });

    expect(update).not.toHaveBeenCalled();
  });
});

// ─── resolveStagedRecord ──────────────────────────────────────────────────────

describe("resolveStagedRecord", () => {
  it("routes to the correct store and sets APPROVED status for APPROVE action", async () => {
    const update = vi.fn().mockResolvedValue({ id: "rec_1", status: StagedRecordStatus.APPROVED });
    const db = {
      stagedResource: { update: vi.fn() },
      stagedClient: { update: vi.fn() },
      stagedProject: { update: vi.fn() },
      stagedAssignment: { update },
      stagedVacation: { update: vi.fn() },
      stagedAvailabilityRule: { update: vi.fn() },
      stagedUnresolvedRecord: { update: vi.fn() },
    };

    const result = await resolveStagedRecord(db, {
      id: "rec_1",
      recordType: DispoStagedRecordType.ASSIGNMENT,
      action: "APPROVE",
    });

    expect(update).toHaveBeenCalledWith({
      where: { id: "rec_1" },
      data: { status: StagedRecordStatus.APPROVED },
    });
    expect(result).toEqual({ id: "rec_1", status: StagedRecordStatus.APPROVED });
    // No other store should have been touched
    expect(db.stagedResource.update).not.toHaveBeenCalled();
  });

  it("routes to stagedUnresolvedRecord and sets REJECTED status for SKIP action", async () => {
    const update = vi.fn().mockResolvedValue({ id: "rec_2", status: StagedRecordStatus.REJECTED });
    const db = {
      stagedResource: { update: vi.fn() },
      stagedClient: { update: vi.fn() },
      stagedProject: { update: vi.fn() },
      stagedAssignment: { update: vi.fn() },
      stagedVacation: { update: vi.fn() },
      stagedAvailabilityRule: { update: vi.fn() },
      stagedUnresolvedRecord: { update },
    };

    await resolveStagedRecord(db, {
      id: "rec_2",
      recordType: DispoStagedRecordType.UNRESOLVED,
      action: "SKIP",
    });

    expect(update).toHaveBeenCalledWith({
      where: { id: "rec_2" },
      data: { status: StagedRecordStatus.REJECTED },
    });
  });
});

// ─── commitImportBatch ────────────────────────────────────────────────────────

describe("commitImportBatch", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    vi.mocked(commitDispoImportBatch).mockResolvedValue({ resources: 5, projects: 3 } as never);
  });

  it("delegates to the application layer with the provided options", async () => {
    const db = { marker: "db" };

    const result = await commitImportBatch(db as never, {
      importBatchId: "batch_1",
      allowTbdUnresolved: true,
      importTbdProjects: false,
    });

    expect(commitDispoImportBatch).toHaveBeenCalledWith(db, {
      importBatchId: "batch_1",
      allowTbdUnresolved: true,
      importTbdProjects: false,
    });
    expect(result).toEqual({ resources: 5, projects: 3 });
  });

  it("creates an audit entry with the commit summary after a successful commit", async () => {
    const db = { marker: "db" };

    await commitImportBatch(db as never, {
      importBatchId: "batch_1",
      userId: "user_admin",
    });

    expect(createAuditEntry).toHaveBeenCalledWith(
      expect.objectContaining({
        entityType: "ImportBatch",
        entityId: "batch_1",
        action: "IMPORT",
        summary: 'Committed import batch ({"resources":5,"projects":3})',
        userId: "user_admin",
      }),
    );
  });
});

// ─── listStagedAssignments / listStagedUnresolvedRecords (spot-check) ─────────

describe("listStagedAssignments", () => {
  it("filters by resourceExternalId when provided", async () => {
    const findMany = vi.fn().mockResolvedValue([]);
    const db = { stagedAssignment: { findMany } };

    await listStagedAssignments(db, {
      importBatchId: "batch_1",
      limit: 10,
      resourceExternalId: "R-007",
    });

    expect(findMany).toHaveBeenCalledWith(
      expect.objectContaining({
        where: { importBatchId: "batch_1", resourceExternalId: "R-007" },
      }),
    );
  });
});

describe("listStagedUnresolvedRecords", () => {
  it("filters by recordType when provided", async () => {
    const findMany = vi.fn().mockResolvedValue([]);
    const db = { stagedUnresolvedRecord: { findMany } };

    await listStagedUnresolvedRecords(db, {
      importBatchId: "batch_1",
      limit: 10,
      recordType: DispoStagedRecordType.PROJECT,
    });

    expect(findMany).toHaveBeenCalledWith(
      expect.objectContaining({
        where: { importBatchId: "batch_1", recordType: DispoStagedRecordType.PROJECT },
      }),
    );
  });
});
