import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import { db } from "@/lib/db";
import { errorReportItemFromRow, errorReportKeyFromRow } from "@/lib/db/error-report-mapping";
import {
  applyFixtureErrorReportPatch,
  resetFixtureErrorReportOverrides,
  withFixtureErrorReportOverride,
} from "@/lib/db/fixture-error-report-overrides";
import { PAGINATION, PRECHECK_MINUTES_BEFORE } from "@/lib/config";
import { errorReportRows, errorReportTimetables } from "@/lib/fixtures/error-reports";
import { facilities } from "@/lib/fixtures/facilities";
import type { ErrorReportDbQuery, PrecheckStatusPatch } from "@/lib/types";

/**
 * fixtures のエラー一覧（`lib/fixtures/error-reports.ts`）と上書き表（`lib/db/fixture-error-report-overrides.ts`）
 * （指示書AW 段階3-6-2）。
 *
 * ★`fixtureSource` を通す★ — `USE_FIXTURES` を `true` に固定して `db` を読む
 *   （`lib/db/index.ts` の `db` はモジュール読み込み時に束縛される）。
 * ★テストごとに `resetFixtureErrorReportOverrides()` で空に戻す★。
 */

vi.mock("@/lib/db/use-fixtures", () => ({ USE_FIXTURES: true }));

const BASE: ErrorReportDbQuery = {
  include_resolved: false,
  facility_ids: null,
  date_from: null,
  date_to: null,
  error_type: "",
  page: 1,
  per_page: PAGINATION.defaultPerPage,
};

const timetableById = new Map(errorReportTimetables.map((t) => [t.id.toString(), t]));
const timetableFor = (id: bigint | null) => (id === null ? undefined : timetableById.get(id.toString()));

beforeEach(() => {
  resetFixtureErrorReportOverrides();
});

afterEach(() => {
  vi.useRealTimers();
  resetFixtureErrorReportOverrides();
});

describe("fixtures の11行（lib/fixtures/error-reports.ts）", () => {
  it("11行・id は FX-ERR-01〜11 で重複が無い", () => {
    const ids = errorReportRows.map((r) => r.error_id);
    expect(ids).toHaveLength(11);
    expect(new Set(ids).size).toBe(11);
    expect(ids).toEqual(Array.from({ length: 11 }, (_, i) => `FX-ERR-${String(i + 1).padStart(2, "0")}`));
  });

  it.each(errorReportRows.map((r) => [r.error_id, r] as const))(
    "%s は errorReportKeyFromRow() / errorReportItemFromRow() を例外なく通る",
    (_id, row) => {
      expect(() => errorReportKeyFromRow(row)).not.toThrow();
      expect(() => errorReportItemFromRow(row, timetableFor(row.clean_timetable_id))).not.toThrow();
    }
  );

  it("施設名は facilities から id で引いた値（直書きしない）", () => {
    for (const row of errorReportRows) {
      const facility = facilities.find((f) => BigInt(f.id) === row.facility_id);
      expect(row.facility.facility_name).toBe(facility?.facility_name);
    }
  });

  it("表1 との対応: 予定の4区分・生値のシリアル・種類", async () => {
    const { items } = await db.errorReports({ ...BASE, include_resolved: true });
    const byId = new Map(items.map((item) => [item.id, item]));
    expect(byId.get("FX-ERR-01")?.schedule_link).toBe("main");
    expect(byId.get("FX-ERR-02")).toMatchObject({
      schedule_link: "precheck",
      precheck_minutes: PRECHECK_MINUTES_BEFORE[1],
      resolution_status: "in_progress",
    });
    expect(byId.get("FX-ERR-03")?.resolution_status).toBe("resolved");
    expect(byId.get("FX-ERR-04")).toMatchObject({ schedule_link: "none", clean_timetable_id: null });
    expect(byId.get("FX-ERR-05")?.schedule_link).toBe("unknown");
    expect(byId.get("FX-ERR-06")?.schedule_link).toBe("main");
    // E7: errored_at が NULL → 検知時刻は created_at
    const e7 = errorReportRows.find((r) => r.error_id === "FX-ERR-07");
    expect(e7?.errored_at).toBeNull();
    expect(byId.get("FX-ERR-07")?.detected_at).toBe(errorReportKeyFromRow(e7!).detected_at);
    expect(byId.get("FX-ERR-08")).toMatchObject({ serial_number: "0", error_type: "SECURITY_ON" });
    // ★10 は設定に無い分数（生の分数で出す）★
    expect(byId.get("FX-ERR-09")).toMatchObject({ schedule_link: "precheck", precheck_minutes: 10 });
    expect(byId.get("FX-ERR-10")).toMatchObject({ serial_number: "DS-001", error_type: "D-S_CONTROL_FAILURE" });
    expect(byId.get("FX-ERR-11")).toMatchObject({ serial_number: "0", error_type: "NOT_STARTED", error_level: "CRITICAL" });
  });

  it("robot_id が NULL の行（制御盤・セキュリティ・清掃開始失敗）を持つ", () => {
    const nullRobot = errorReportRows.filter((r) => r.robot_id === null).map((r) => r.error_id);
    expect(nullRobot).toEqual(["FX-ERR-08", "FX-ERR-10", "FX-ERR-11"]);
  });
});

describe("fixtureSource.errorReports()", () => {
  it("★既定で10行（FX-ERR-03 は出ない）・「解消済みも表示」で11行★", async () => {
    const def = await db.errorReports(BASE);
    expect(def.total).toBe(10);
    expect(def.items.map((i) => i.id)).not.toContain("FX-ERR-03");
    const all = await db.errorReports({ ...BASE, include_resolved: true });
    expect(all.total).toBe(11);
    expect(all.items).toHaveLength(11);
  });

  it("並びは検知時刻の降順", async () => {
    const { items } = await db.errorReports({ ...BASE, include_resolved: true });
    const times = items.map((i) => Date.parse(i.detected_at));
    expect(times).toEqual([...times].sort((a, b) => b - a));
  });

  it("ページ分け: total はページ分け前・範囲外のページは空", async () => {
    const page2 = await db.errorReports({ ...BASE, per_page: 3, page: 2 });
    const all = await db.errorReports(BASE);
    expect(page2.total).toBe(10);
    expect(page2.items.map((i) => i.id)).toEqual(all.items.slice(3, 6).map((i) => i.id));
    const far = await db.errorReports({ ...BASE, page: 99 });
    expect(far).toEqual({ items: [], total: 10 });
  });

  it("★facility_ids = [] → 0件（罠1）★", async () => {
    expect(await db.errorReports({ ...BASE, facility_ids: [] })).toEqual({ items: [], total: 0 });
  });

  it("error_type=secur → FX-ERR-08 だけ", async () => {
    const { items } = await db.errorReports({ ...BASE, error_type: "secur" });
    expect(items.map((i) => i.id)).toEqual(["FX-ERR-08"]);
  });
});

describe("上書き表（applyFixtureErrorReportPatch）", () => {
  it("★パッチが errorReports() / errorReportById() に反映される（事前チェック以外の行にも書ける）★", async () => {
    vi.useFakeTimers({ toFake: ["Date"] });
    vi.setSystemTime(new Date("2026-10-06T09:00:00+09:00"));
    const patch: PrecheckStatusPatch = { status: "in_progress", responded_at: new Date(), note: "現場に連絡" };
    expect(applyFixtureErrorReportPatch(errorReportRows, "FX-ERR-08", patch)).toEqual({ matched: 1 });

    const item = await db.errorReportById("FX-ERR-08");
    expect(item).toMatchObject({
      resolution_status: "in_progress",
      responded_at: "2026-10-06T09:00:00+09:00",
      note: "現場に連絡",
    });
    const { items } = await db.errorReports(BASE);
    expect(items.find((i) => i.id === "FX-ERR-08")).toEqual(item);
  });

  it("db.updateErrorReportColumns() も同じ上書き表に書く・解消にすると既定の一覧から外れる", async () => {
    expect(await db.updateErrorReportColumns("FX-ERR-01", { status: "resolved" })).toEqual({ matched: 1 });
    expect((await db.errorReports(BASE)).total).toBe(9);
    expect((await db.errorReportById("FX-ERR-01"))?.resolution_status).toBe("resolved");
  });

  it("上書きは重なる（後のパッチで変えない列は前の値のまま）・null は消す", async () => {
    applyFixtureErrorReportPatch(errorReportRows, "FX-ERR-02", { note: "一回目" });
    applyFixtureErrorReportPatch(errorReportRows, "FX-ERR-02", { status: "resolved", resolved_at: new Date("2026-10-06T10:00:00+09:00") });
    let item = await db.errorReportById("FX-ERR-02");
    expect(item).toMatchObject({ note: "一回目", resolution_status: "resolved", resolved_at: "2026-10-06T10:00:00+09:00" });
    applyFixtureErrorReportPatch(errorReportRows, "FX-ERR-02", { note: null });
    item = await db.errorReportById("FX-ERR-02");
    expect(item?.note).toBeNull();
  });

  it("★元の配列の要素は書き換えない★", () => {
    const before = errorReportRows.find((r) => r.error_id === "FX-ERR-01");
    const snapshot = { ...before };
    applyFixtureErrorReportPatch(errorReportRows, "FX-ERR-01", { status: "resolved", note: "x" });
    expect(before).toEqual(snapshot);
    expect(withFixtureErrorReportOverride(before!)).not.toBe(before);
  });

  it("resetFixtureErrorReportOverrides() で元に戻る", async () => {
    applyFixtureErrorReportPatch(errorReportRows, "FX-ERR-04", { status: "resolved" });
    expect((await db.errorReportById("FX-ERR-04"))?.resolution_status).toBe("resolved");
    resetFixtureErrorReportOverrides();
    expect((await db.errorReportById("FX-ERR-04"))?.resolution_status).toBe("unresolved");
  });

  it("★存在しない id・大小文字違いは matched: 0（errorReportById() も null）★", async () => {
    expect(applyFixtureErrorReportPatch(errorReportRows, "FX-ERR-99", { status: "resolved" })).toEqual({ matched: 0 });
    expect(applyFixtureErrorReportPatch(errorReportRows, "fx-err-01", { status: "resolved" })).toEqual({ matched: 0 });
    expect(await db.errorReportById("fx-err-01")).toBeNull();
    expect((await db.errorReportById("FX-ERR-01"))?.resolution_status).toBe("unresolved");
  });

  it("★4つ以外のキー・空パッチは例外（Prisma 版と同じ歯止め）★", () => {
    const bad = { status: "resolved", updated_at: new Date() } as unknown as PrecheckStatusPatch;
    expect(() => applyFixtureErrorReportPatch(errorReportRows, "FX-ERR-01", bad)).toThrow();
    expect(() => applyFixtureErrorReportPatch(errorReportRows, "FX-ERR-01", {})).toThrow();
  });
});
