import { beforeEach, describe, expect, it, vi } from "vitest";

import { db } from "@/lib/db";
import { FACILITY_SECTION_VISIBLE_WHERE } from "@/lib/db/visibility";
import { logEvent } from "@/lib/logger";

/**
 * `prismaSource.schedules()`（`GET /schedules`・ダッシュボードの予定件数）の期待値表（指示書BB）。
 *
 * | # | 入力 | 期待 |
 * |---|---|---|
 * | R1 | 清掃エリアに有効なロボット1台 | 今どおり（ログなし） |
 * | R4 | ★0台★ | ★例外にならない★。`serial_number: null`。`data.anomaly`（`active_robots_per_section`・`actual: 0`）は★清掃エリアごとに1回★（`clean_schedule` の行ごとではない） |
 * | R5 | 2台（id 10 と id 7） | `orderBy: { id: "asc" }` を SQL に頼む（並びはスタブが再現）→ id 7。ログ（`actual: 2`）1回 |
 *
 * ★DBには接続しません★ — `getPrisma()` をスタブに差し替え、`USE_FIXTURES` を `false` に固定して `prismaSource` を走らせる。
 */

interface StubRobot {
  id: bigint;
  serial_number: string;
}

interface StubSchedule {
  id: bigint;
  facility_id: bigint;
  section_id: bigint;
  weeks: string;
  start_time: string;
  end_time: string;
  inactive: number;
  deleted_at: Date | null;
  facility: { facility_name: string };
  section: { robots: StubRobot[] };
}

let schedules: StubSchedule[];
const calls: Record<string, unknown>[] = [];

vi.mock("@/lib/db/use-fixtures", () => ({ USE_FIXTURES: false }));

vi.mock("@/lib/logger", async (importOriginal) => {
  const actual = await importOriginal<typeof import("@/lib/logger")>();
  return { ...actual, logEvent: vi.fn() };
});

vi.mock("@/lib/db/prisma", () => ({
  getPrisma: async () => ({
    cleanSchedule: {
      findMany: async (args: Record<string, unknown>) => {
        calls.push(args);
        // ★`where` は評価しない（条件の中身は下のテストで `args.where` を見る）★。
        //   ★`include.section.include.robots.orderBy` が `{ id: "asc" }` のときだけ並べ替える★（付け忘れたら R5 が赤）
        const include = args.include as { section?: { include?: { robots?: { orderBy?: { id?: string } } } } };
        const sortRobots = include.section?.include?.robots?.orderBy?.id === "asc";
        return schedules.map((s) => ({
          ...s,
          section: {
            robots: sortRobots
              ? [...s.section.robots].sort((a, b) => Number(a.id) - Number(b.id))
              : s.section.robots,
          },
        }));
      },
    },
  }),
}));

/** ★実在の店名を使わない★（`CLAUDE.md` §7） */
function schedule(id: number, sectionId: number, robots: StubRobot[]): StubSchedule {
  return {
    id: BigInt(id),
    facility_id: BigInt(1),
    section_id: BigInt(sectionId),
    weeks: "1111111",
    start_time: "0200",
    end_time: "0300",
    inactive: 0,
    deleted_at: null,
    facility: { facility_name: "バリューマート北町店" },
    section: { robots },
  };
}

function robotAnomalies() {
  return vi
    .mocked(logEvent)
    .mock.calls.filter(([, p]) => p.event === "data.anomaly" && p.metric === "active_robots_per_section")
    .map(([, p]) => p);
}

beforeEach(() => {
  vi.mocked(logEvent).mockClear();
  calls.length = 0;
  schedules = [schedule(21, 10, [{ id: BigInt(1), serial_number: "8111R5628060011" }])];
});

describe("prismaSource.schedules — 有効なロボットがいない清掃エリア（指示書BB U2・表2）", () => {
  it("R1: 1台なら今どおり（ログを出さない）", async () => {
    const rows = await db.schedules();
    expect(rows.map((r) => [r.id, r.serial_number])).toEqual([[21, "8111R5628060011"]]);
    expect(robotAnomalies()).toHaveLength(0);
  });

  it("★R4: 0台でも例外にならず `serial_number: null`。ログは清掃エリアごとに1回（行ごとではない）★", async () => {
    schedules = [schedule(21, 10, []), schedule(22, 10, []), schedule(23, 20, [{ id: BigInt(2), serial_number: "S-2" }])];
    const rows = await db.schedules();
    expect(rows.map((r) => [r.id, r.serial_number])).toEqual([
      [21, null],
      [22, null],
      [23, "S-2"],
    ]);
    expect(robotAnomalies()).toEqual([
      { event: "data.anomaly", metric: "active_robots_per_section", expected: 1, actual: 0, section_id: "10" },
    ]);
  });

  it("★R5: 2台（id 10 と id 7）→ id 7 を採る。ログ（actual: 2）は1回★", async () => {
    schedules = [
      schedule(21, 10, [
        { id: BigInt(10), serial_number: "SERIAL-ID10" },
        { id: BigInt(7), serial_number: "SERIAL-ID7" },
      ]),
    ];
    const rows = await db.schedules();
    expect(rows[0].serial_number).toBe("SERIAL-ID7");
    expect(robotAnomalies()).toEqual([
      { event: "data.anomaly", metric: "active_robots_per_section", expected: 1, actual: 2, section_id: "10" },
    ]);
  });
});

describe("prismaSource.schedules — 削除済みの施設・清掃エリア（指示書BB U1・表1 V5）", () => {
  it("★`where` は `clean_schedules.deleted_at` に加えて施設・清掃エリアの可視性★", async () => {
    await db.schedules();
    expect(calls[0].where).toEqual({ deleted_at: null, ...FACILITY_SECTION_VISIBLE_WHERE });
  });
});
