import { beforeEach, describe, expect, it, vi } from "vitest";

import { db } from "@/lib/db";
import { dsDevices as fixtureDsDevices } from "@/lib/fixtures/ds-devices";

/**
 * 指示書AH 段階1 — ★`dsDevices()` が `control_reports` を全件読むのをやめた★ことの固定。
 *
 * ------------------------------------------------------------------------
 * ★★ローカルDBでは絶対に検出できません★★
 * ------------------------------------------------------------------------
 * ローカルの `control_reports` は ★実測14行★。以前の実装（`include` + `take: 1`）でも
 * 何も起きません。★★`take: 1` は1文字もSQLに出ず、Prisma が制御盤ぶんの行を
 * 全部取り寄せてからアプリ側で1件に切っているだけ★★（実測）なので、
 * ★行数が増えた日（想定1,100万行）に初めて `JavaScript heap out of memory` で落ちます★
 * （実測: 100万行で 2.7GB / ★300万行で即死★）。
 *
 * ★だから「結果」ではなく「発行したクエリ」を固定します★（#2）。
 * ★結果だけを見るテストは、`include` に戻しても緑のままです★。
 *
 * ------------------------------------------------------------------------
 * ★★DBには接続しません★★
 * ------------------------------------------------------------------------
 * `@/lib/db/prisma` の `getPrisma()` をスタブに差し替え、`USE_FIXTURES` を `false` に
 * 固定して ★`prismaSource` の経路★ を走らせます
 * （`app/api/clean-logs/__tests__/route.test.ts` と同じ作法）。
 */

// ---------------------------------------------------------------- スタブ（DBの代わり）

/** `controls` の1行（★`include: { facility: true }` 済みの形で持つ★） */
interface StubControl {
  id: bigint;
  serial_number: string;
  facility_id: bigint;
  status: number;
  deleted_at: Date | null;
  facility: { facility_name: string };
}

/** `control_reports` の1行（★このテストが使う列だけ★） */
interface StubReport {
  control_id: bigint;
  reported_at: Date;
}

interface StubData {
  controls: StubControl[];
  reports: StubReport[];
}

/** 発行されたクエリの記録（★`controlReport.findMany` が0回であることを見る★） */
const calls: {
  controlFindMany: Record<string, unknown>[];
  controlReportGroupBy: Record<string, unknown>[];
  controlReportFindMany: Record<string, unknown>[];
} = { controlFindMany: [], controlReportGroupBy: [], controlReportFindMany: [] };

let data: StubData;

vi.mock("@/lib/db/use-fixtures", () => ({ USE_FIXTURES: false }));

vi.mock("@/lib/db/prisma", () => ({
  getPrisma: async () => ({
    control: {
      findMany: async (args?: Record<string, unknown>) => {
        calls.controlFindMany.push(args ?? {});
        const where = (args?.where ?? {}) as { deleted_at?: Date | null };
        // ★`deleted_at: null` の絞り込みを**わざと**実装している★ —
        //   条件を落としたら #4 が赤くなるように
        const rows =
          "deleted_at" in where && where.deleted_at === null
            ? data.controls.filter((c) => c.deleted_at === null)
            : data.controls;
        return rows.map((c) => ({ ...c }));
      },
    },
    controlReport: {
      // ★呼ばれてはいけない経路★（呼ばれたら #2 が赤くなる）
      findMany: async (args?: Record<string, unknown>) => {
        calls.controlReportFindMany.push(args ?? {});
        return data.reports.map((r) => ({ ...r }));
      },
      groupBy: async (args?: Record<string, unknown>) => {
        calls.controlReportGroupBy.push(args ?? {});
        const where = (args?.where ?? {}) as {
          control_id?: { in?: readonly bigint[] };
        };
        const ids = where.control_id?.in;
        if (ids === undefined) {
          throw new Error(
            "[stub] where.control_id.in が undefined です。★罠1★: Prisma では条件が消えて全件になります"
          );
        }
        const wanted = new Set(ids.map((id) => String(id)));
        const maxByControl = new Map<string, { control_id: bigint; reported_at: Date }>();
        for (const row of data.reports) {
          const key = String(row.control_id);
          if (!wanted.has(key)) continue;
          const current = maxByControl.get(key);
          if (!current || current.reported_at.getTime() < row.reported_at.getTime()) {
            maxByControl.set(key, { control_id: row.control_id, reported_at: row.reported_at });
          }
        }
        // ★Prisma と同じく `control_id` は `BigInt` のまま返す★ —
        //   ★`String()` でキーを揃えるのをやめたら（罠1）#1 が例外で落ちる★
        return [...maxByControl.values()].map((g) => ({
          control_id: g.control_id,
          _max: { reported_at: g.reported_at },
        }));
      },
    },
  }),
}));

// ---------------------------------------------------------------- 合成データ

/** DB に入っているのと同じ「壁時計値としての `Date`」（D-115。他のテストと同じヘルパー） */
function dbWallClock(jstIso: string): Date {
  return new Date(jstIso.replace("+09:00", "Z"));
}

/** ★実在の店名を使わない★（`CLAUDE.md` §7 / `docs/fixtures.md`） */
function control(
  id: number,
  serial: string,
  facilityId: number,
  facilityName: string,
  status: number,
  deletedAt: Date | null = null
): StubControl {
  return {
    id: BigInt(id),
    serial_number: serial,
    facility_id: BigInt(facilityId),
    status,
    deleted_at: deletedAt,
    facility: { facility_name: facilityName },
  };
}

function report(controlId: number, jstIso: string): StubReport {
  return { control_id: BigInt(controlId), reported_at: dbWallClock(jstIso) };
}

function buildData(): StubData {
  return {
    controls: [
      control(1, "DS-001", 1, "メガバリューマート中央本店", 0),
      control(2, "DS-002", 3, "メガバリューマート東口店", 1),
      // ★論理削除済み★（#4）
      control(
        3,
        "DS-003",
        4,
        "バリューマート西通り店",
        0,
        dbWallClock("2026-09-01T00:00:00+09:00")
      ),
    ],
    reports: [
      // ★制御盤1は3件。★最新は 09:05★（古い行に引きずられないこと）
      report(1, "2026-09-14T03:42:00+09:00"),
      report(1, "2026-09-14T09:05:00+09:00"),
      report(1, "2026-09-13T22:10:00+09:00"),
      report(2, "2026-09-14T08:58:00+09:00"),
      // ★論理削除された制御盤の心拍★（一覧から消えるので使われない）
      report(3, "2026-09-14T09:00:00+09:00"),
    ],
  };
}

beforeEach(() => {
  data = buildData();
  calls.controlFindMany = [];
  calls.controlReportGroupBy = [];
  calls.controlReportFindMany = [];
});

describe("prismaSource.dsDevices（指示書AH 段階1 / D-1〜D-3）", () => {
  it("#1 通常 — ★制御盤ごとに最新の `reported_at` を返す★（従来と同じ `DsDevice[]`）", async () => {
    const rows = await db.dsDevices();

    expect(rows).toEqual([
      {
        serial_number: "DS-001",
        facility_id: 1,
        facility_name: "メガバリューマート中央本店",
        connection_status: "ok",
        // ★3件のうち最大★（古い2件に引きずられない）
        last_reported_at: "2026-09-14T09:05:00+09:00",
        note: null,
      },
      {
        serial_number: "DS-002",
        facility_id: 3,
        facility_name: "メガバリューマート東口店",
        // ★controls.status=1 → error（D-065 Q1-2）★
        connection_status: "error",
        last_reported_at: "2026-09-14T08:58:00+09:00",
        note: null,
      },
    ]);
    // ★`facility_id` は `number`★（`BigInt` を残すと `NextResponse.json` が例外を投げる）
    expect(typeof rows[0].facility_id).toBe("number");
  });

  it("#2 ★発行クエリの検査★ — `controlReport.findMany` が0回 / `groupBy` が1回", async () => {
    await db.dsDevices();

    // ★★これが本作業の核心★★ — 行を1行も運ばないこと
    expect(calls.controlReportFindMany).toHaveLength(0);
    expect(calls.controlReportGroupBy).toHaveLength(1);
    expect(calls.controlFindMany).toHaveLength(1);

    // ★`control_reports` の入れ子 include に戻っていないこと★
    //   （戻すと `take: 1` がSQLに降りず、制御盤ぶんの行を全部読む）
    const include = calls.controlFindMany[0].include as Record<string, unknown>;
    expect(Object.keys(include)).toEqual(["facility"]);

    const groupBy = calls.controlReportGroupBy[0];
    expect(groupBy.by).toEqual(["control_id"]);
    // ★★`MAX(id)` に変えないこと（★罠3★ / D-3）★★ — `id` の大小順と
    //   `reported_at` の大小順が一致する保証が無い（TeamWorks の書き込み順は未確認）
    expect(groupBy._max).toEqual({ reported_at: true });
    // ★★時間の足切りを足さないこと（★罠2★ / D-2）★★ —
    //   `reported_at` に索引が無く走査は減らないうえ、
    //   ★足切りの外にしか心拍が無い制御盤の最終通信が空欄になる★
    expect(Object.keys(groupBy.where as object)).toEqual(["control_id"]);
    // ★★`take` を足さないこと（★罠8★）★★ — SQLに降りない
    expect(groupBy.take).toBeUndefined();
    // ★論理削除された制御盤は `in` にも入らない★
    const ids = (groupBy.where as { control_id: { in: bigint[] } }).control_id.in;
    expect(ids.map(String)).toEqual(["1", "2"]);
  });

  it("#3 ★制御盤に心拍が0件★ — 従来と同じ例外（メッセージも変えない）", async () => {
    // 制御盤2の心拍だけを消す（★制御盤1には残す＝「1台でも欠けたら例外」の挙動★）
    data.reports = data.reports.filter((r) => String(r.control_id) !== "2");

    await expect(db.dsDevices()).rejects.toThrow(
      "[db] dsDevices: controls(id=2) に control_reports が1件もありません"
    );
  });

  it("#4 ★論理削除された制御盤は一覧に出ない★（従来どおり。`deleted_at` を落とさない）", async () => {
    const rows = await db.dsDevices();

    expect(rows.map((r) => r.serial_number)).toEqual(["DS-001", "DS-002"]);
    // ★`inactive` では絞らない★（無効化は削除ではない。指示書P 禁止事項10 / D-086）ので
    //   `where` のキーは `deleted_at` だけ
    expect(calls.controlFindMany[0].where).toEqual({ deleted_at: null });
  });

  it("#5 `USE_FIXTURES` true/false で ★同じ形の `DsDevice`★ を返す（禁止事項6）", async () => {
    // ★fixtures 版（`fixtureSource.dsDevices()`）は `lib/fixtures/ds-devices.ts` を
    //   そのまま返す★ので、その1件を Prisma 経路の合成データで再現して突き合わせる。
    const fixture = fixtureDsDevices.find((d) => d.serial_number === "DS-001");
    if (!fixture) throw new Error("fixtures に DS-001 がありません");

    data = {
      controls: [
        control(
          1,
          fixture.serial_number,
          fixture.facility_id,
          fixture.facility_name,
          fixture.connection_status === "ok" ? 0 : 1
        ),
      ],
      reports: [report(1, fixture.last_reported_at)],
    };

    const rows = await db.dsDevices();
    expect(rows).toHaveLength(1);

    // ★キーの集合が一致すること★（片方にだけフィールドがある状態を作らない）
    expect(Object.keys(rows[0]).sort()).toEqual(Object.keys(fixture).sort());
    // ★`note` 以外は一致すること★ —
    //   ★Prisma 経路の `note` は常に `null`★（DBに対応カラムが無い。D-065 Q1-1）で、
    //   実値は `app/api/ds-devices/route.ts` がJSONストアからマージする。
    //   ★これは実装漏れではなく意図した非対称★。
    expect({ ...rows[0], note: null }).toEqual({ ...fixture, note: null });
  });
});
