import { BlueprintTarget, FieldType, SystemRole } from "@nexus/shared";
import { describe, expect, it, vi, beforeEach } from "vitest";
import { blueprintRouter } from "../router/blueprint.js";
import { createCallerFactory } from "../trpc.js";

// ─── Mocks ────────────────────────────────────────────────────────────────────

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

// ─── Caller factory ───────────────────────────────────────────────────────────

const createCaller = createCallerFactory(blueprintRouter);

function createAdminCaller(db: Record<string, unknown>) {
  return createCaller({
    session: {
      user: { email: "admin@example.com", name: "Admin", image: null },
      expires: "2099-01-01T00:00:00.000Z",
    },
    db: db as never,
    dbUser: {
      id: "user_admin",
      systemRole: SystemRole.ADMIN,
      permissionOverrides: null,
    },
    roleDefaults: null,
  });
}

function createManagerCaller(db: Record<string, unknown>) {
  return createCaller({
    session: {
      user: { email: "manager@example.com", name: "Manager", image: null },
      expires: "2099-01-01T00:00:00.000Z",
    },
    db: db as never,
    dbUser: {
      id: "user_manager",
      systemRole: SystemRole.MANAGER,
      permissionOverrides: null,
    },
    roleDefaults: null,
  });
}

function sampleBlueprint(overrides: Record<string, unknown> = {}) {
  return {
    id: "bp_1",
    name: "Consulting Blueprint",
    target: BlueprintTarget.PROJECT,
    description: "Default setup",
    fieldDefs: [],
    defaults: {},
    validationRules: [],
    rolePresets: [],
    isActive: true,
    isGlobal: false,
    createdAt: new Date("2026-01-01T00:00:00.000Z"),
    updatedAt: new Date("2026-01-01T00:00:00.000Z"),
    deletedAt: null,
    ...overrides,
  };
}

// ─── Tests ────────────────────────────────────────────────────────────────────

describe("blueprint.list", () => {
  it("returns active blueprints ordered by name for the given target", async () => {
    const findMany = vi
      .fn()
      .mockResolvedValue([
        sampleBlueprint({ id: "bp_1", name: "Feature Film" }),
        sampleBlueprint({ id: "bp_2", name: "Short Film" }),
      ]);

    const caller = createManagerCaller({ blueprint: { findMany } });

    const result = await caller.list({ target: BlueprintTarget.PROJECT });

    expect(findMany).toHaveBeenCalledWith({
      where: { target: BlueprintTarget.PROJECT, isActive: true },
      orderBy: { name: "asc" },
    });
    expect(result).toHaveLength(2);
    expect(result[0]!.name).toBe("Feature Film");
  });

  it("returns only inactive blueprints when isActive=false is requested", async () => {
    const findMany = vi
      .fn()
      .mockResolvedValue([sampleBlueprint({ id: "bp_old", name: "Legacy", isActive: false })]);

    const caller = createManagerCaller({ blueprint: { findMany } });

    const result = await caller.list({ isActive: false });

    expect(findMany).toHaveBeenCalledWith(expect.objectContaining({ where: { isActive: false } }));
    expect(result[0]!.name).toBe("Legacy");
  });

  it("omits the target filter when no target is provided", async () => {
    const findMany = vi.fn().mockResolvedValue([]);
    const caller = createManagerCaller({ blueprint: { findMany } });

    await caller.list({});

    expect(findMany).toHaveBeenCalledWith(expect.objectContaining({ where: { isActive: true } }));
    // target must not appear in the where clause
    const whereArg = (findMany.mock.calls[0]![0] as { where: Record<string, unknown> }).where;
    expect(whereArg).not.toHaveProperty("target");
  });

  it("returns an empty list when no blueprints match the filter", async () => {
    const findMany = vi.fn().mockResolvedValue([]);
    const caller = createManagerCaller({ blueprint: { findMany } });

    const result = await caller.list({ target: BlueprintTarget.PROJECT, isActive: true });

    expect(result).toEqual([]);
  });
});

describe("blueprint.getById", () => {
  it("returns the blueprint when it exists", async () => {
    const findUnique = vi.fn().mockResolvedValue(sampleBlueprint());

    const caller = createManagerCaller({ blueprint: { findUnique } });

    const result = await caller.getById({ id: "bp_1" });

    expect(findUnique).toHaveBeenCalledWith(expect.objectContaining({ where: { id: "bp_1" } }));
    expect(result).toMatchObject({ id: "bp_1", name: "Consulting Blueprint" });
  });

  it("throws NOT_FOUND when the blueprint does not exist", async () => {
    const findUnique = vi.fn().mockResolvedValue(null);

    const caller = createManagerCaller({ blueprint: { findUnique } });

    await expect(caller.getById({ id: "missing_bp" })).rejects.toMatchObject({
      code: "NOT_FOUND",
    });
  });

  it("returns blueprint including fieldDefs and defaults", async () => {
    const fieldDefs = [
      {
        id: "f_1",
        label: "Episode Count",
        key: "episode_count",
        type: FieldType.NUMBER,
        required: true,
        order: 0,
      },
    ];
    const defaults = { episode_count: 10 };

    const findUnique = vi.fn().mockResolvedValue(sampleBlueprint({ fieldDefs, defaults }));
    const caller = createManagerCaller({ blueprint: { findUnique } });

    const result = await caller.getById({ id: "bp_1" });

    expect(result.fieldDefs).toEqual(fieldDefs);
    expect(result.defaults).toEqual(defaults);
  });

  it("rejects when the caller lacks VIEW_PLANNING permission (VIEWER role)", async () => {
    const viewerCaller = createCaller({
      session: {
        user: { email: "viewer@example.com", name: "Viewer", image: null },
        expires: "2099-01-01T00:00:00.000Z",
      },
      db: {} as never,
      dbUser: {
        id: "user_viewer",
        systemRole: SystemRole.VIEWER,
        permissionOverrides: null,
      },
      roleDefaults: null,
    });

    await expect(viewerCaller.getById({ id: "bp_1" })).rejects.toMatchObject({
      code: "FORBIDDEN",
    });
  });
});

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

  it("creates a blueprint with name and target and returns it", async () => {
    const created = sampleBlueprint({ id: "bp_new_1", name: "Episodic", description: null });
    const blueprintCreate = vi.fn().mockResolvedValue(created);
    const auditCreate = vi.fn().mockResolvedValue({});

    const caller = createAdminCaller({
      blueprint: { create: blueprintCreate },
      auditLog: { create: auditCreate },
    });

    const result = await caller.create({
      name: "Episodic",
      target: BlueprintTarget.PROJECT,
    });

    expect(blueprintCreate).toHaveBeenCalledWith({
      data: expect.objectContaining({
        name: "Episodic",
        target: BlueprintTarget.PROJECT,
      }),
    });
    expect(result).toMatchObject({ id: "bp_new_1", name: "Episodic" });
  });

  it("creates a blueprint with optional description", async () => {
    const created = sampleBlueprint({ id: "bp_new_2", description: "Short commercial production" });
    const blueprintCreate = vi.fn().mockResolvedValue(created);
    const auditCreate = vi.fn().mockResolvedValue({});

    const caller = createAdminCaller({
      blueprint: { create: blueprintCreate },
      auditLog: { create: auditCreate },
    });

    const result = await caller.create({
      name: "Commercial",
      target: BlueprintTarget.PROJECT,
      description: "Short commercial production",
    });

    expect(result.description).toBe("Short commercial production");
  });

  it("creates a blueprint with fieldDefs and stores them verbatim", async () => {
    const fieldDefs = [
      {
        id: "f_budget",
        label: "Budget",
        key: "budget",
        type: FieldType.NUMBER,
        required: false,
        order: 0,
      },
    ];
    const created = sampleBlueprint({ id: "bp_new_3", fieldDefs });
    const blueprintCreate = vi.fn().mockResolvedValue(created);
    const auditCreate = vi.fn().mockResolvedValue({});

    const caller = createAdminCaller({
      blueprint: { create: blueprintCreate },
      auditLog: { create: auditCreate },
    });

    const result = await caller.create({
      name: "Budget Template",
      target: BlueprintTarget.PROJECT,
      fieldDefs,
    });

    expect(blueprintCreate).toHaveBeenCalledWith({
      data: expect.objectContaining({ fieldDefs }),
    });
    expect(result.fieldDefs).toEqual(fieldDefs);
  });

  it("rejects creation when the caller is not an admin", async () => {
    await expect(
      createManagerCaller({}).create({
        name: "Blocked Blueprint",
        target: BlueprintTarget.PROJECT,
      }),
    ).rejects.toMatchObject({ code: "FORBIDDEN" });
  });
});

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

  it("updates the blueprint name and description", async () => {
    const existing = sampleBlueprint();
    const updated = sampleBlueprint({ name: "Feature Film v2", description: "Updated" });

    const findUnique = vi.fn().mockResolvedValue(existing);
    const blueprintUpdate = vi.fn().mockResolvedValue(updated);
    const auditCreate = vi.fn().mockResolvedValue({});

    const caller = createAdminCaller({
      blueprint: { findUnique, update: blueprintUpdate },
      auditLog: { create: auditCreate },
    });

    const result = await caller.update({
      id: "bp_1",
      data: { name: "Feature Film v2", description: "Updated" },
    });

    expect(blueprintUpdate).toHaveBeenCalledWith({
      where: { id: "bp_1" },
      data: { name: "Feature Film v2", description: "Updated" },
    });
    expect(result).toMatchObject({ name: "Feature Film v2", description: "Updated" });
  });

  it("updates the blueprint fieldDefs with new definitions", async () => {
    const newFieldDefs = [
      {
        id: "f_genre",
        label: "Genre",
        key: "genre",
        type: FieldType.TEXT,
        required: true,
        order: 0,
      },
    ];
    const existing = sampleBlueprint();
    const updated = sampleBlueprint({ fieldDefs: newFieldDefs });

    const findUnique = vi.fn().mockResolvedValue(existing);
    const blueprintUpdate = vi.fn().mockResolvedValue(updated);
    const auditCreate = vi.fn().mockResolvedValue({});

    const caller = createAdminCaller({
      blueprint: { findUnique, update: blueprintUpdate },
      auditLog: { create: auditCreate },
    });

    const result = await caller.update({ id: "bp_1", data: { fieldDefs: newFieldDefs } });

    expect(result.fieldDefs).toEqual(newFieldDefs);
  });

  it("throws NOT_FOUND when the blueprint to update does not exist", async () => {
    const findUnique = vi.fn().mockResolvedValue(null);

    const caller = createAdminCaller({ blueprint: { findUnique } });

    await expect(
      caller.update({ id: "missing_bp", data: { name: "Ghost" } }),
    ).rejects.toMatchObject({ code: "NOT_FOUND" });
  });

  it("sends only changed fields in the update payload (partial update)", async () => {
    const existing = sampleBlueprint({ description: "Original description" });
    const updated = sampleBlueprint({ name: "Renamed Film" });

    const findUnique = vi.fn().mockResolvedValue(existing);
    const blueprintUpdate = vi.fn().mockResolvedValue(updated);
    const auditCreate = vi.fn().mockResolvedValue({});

    const caller = createAdminCaller({
      blueprint: { findUnique, update: blueprintUpdate },
      auditLog: { create: auditCreate },
    });

    await caller.update({ id: "bp_1", data: { name: "Renamed Film" } });

    const updateData = (blueprintUpdate.mock.calls[0]![0] as { data: Record<string, unknown> })
      .data;
    expect(updateData).toHaveProperty("name", "Renamed Film");
    // description was not included in the patch so it must not appear in the data payload
    expect(updateData).not.toHaveProperty("description");
  });
});
