/**
 * `lib/db/precheck-mapping.ts` の期待値表（指示書S 3-5 / CLAUDE.md §9.4）
 *
 * ★DB 依存のテストは書かない★（禁止事項12）。すべて純粋関数への入出力。
 * ★閾値・コード値は `lib/config` から取り、テストにも直書きしない★（禁止事項3）。
 */
import { describe, expect, it } from "vitest";
import { CLEAN_TIMETABLE_PRECHECK, PRECHECK_MINUTES_BEFORE } from "@/lib/config";
import type { RobotErrorLike } from "@/lib/db/clean-log-mapping";
import { robotStatusFromErrors } from "@/lib/db/clean-log-mapping";
import {
  detectedAtFromError,
  mainRunSucceededFromReport,
  precheckItemFromErrorRow,
  precheckMinutesFromCode,
  resolutionStatusFromRobotErrorStatus,
  robotErrorStatusFromResolutionStatus,
  statusColumnsFromPrecheckRows,
} from "@/lib/db/precheck-mapping";
import { prechecks as fixturePrechecks } from "@/lib/fixtures/prechecks";
import { judgeGrade } from "@/lib/judge";
import type { PrecheckStatus } from "@/lib/types";

/** 待機中（エラー無し）の期待値。`ROBOT_STATUS_STANDBY` を直書きしない */
const STANDBY = robotStatusFromErrors([]);

describe("precheckMinutesFromCode", () => {
  // 期待値表（指示書X 段階1 1-1。★境界値★）
  //   本清掃              → null（事前チェック行ではない = アラートの対象外）
  //   0 以外              → 生値をそのまま「分前」として返す（TeamWorks 第7回 A5-2/A5-5。D-112）
  //   負の値・想定外の値  → 例外にせず生値をそのまま返す（丸めない。D-077/D-089項3/D-104項2）
  it("本清掃行は null（事前チェック行ではない）", () => {
    expect(precheckMinutesFromCode(CLEAN_TIMETABLE_PRECHECK.main)).toBeNull();
  });

  it("★『1』は1分前（従来は60分前と読んでいたが、TeamWorks第7回A5-3で真逆と判明。2026-09-11）★", () => {
    // 意図した仕様変更: 以前はコード値の1回目とみなして PRECHECK_MINUTES_BEFORE[0]（60）を
    // 返していたが、A5-3「もし1や2が格納されていたら1分, 2分前としてください」により、
    // 1はそのまま1分前が正しい。
    expect(precheckMinutesFromCode(1)).toBe(1);
  });

  it("★『2』は2分前（従来は30分前と読んでいたが、TeamWorks第7回A5-3で真逆と判明。2026-09-11）★", () => {
    expect(precheckMinutesFromCode(2)).toBe(2);
  });

  it("PRECHECK_MINUTES_BEFORE[0]（実DBに実際に入る値。A5-2）はその値のまま", () => {
    expect(precheckMinutesFromCode(PRECHECK_MINUTES_BEFORE[0])).toBe(
      PRECHECK_MINUTES_BEFORE[0]
    );
  });

  it("PRECHECK_MINUTES_BEFORE[1]（同上）はその値のまま", () => {
    expect(precheckMinutesFromCode(PRECHECK_MINUTES_BEFORE[1])).toBe(
      PRECHECK_MINUTES_BEFORE[1]
    );
  });

  it("設定値以外の分数も生値をそのまま返す（20なら20分前。D-093 項2のユーザー原文）", () => {
    expect(precheckMinutesFromCode(20)).toBe(20);
  });

  it("tinyint unsigned の上限（255）も丸めない・その他に寄せない", () => {
    expect(precheckMinutesFromCode(255)).toBe(255);
  });

  it("★負の値も例外にせず生値をそのまま返す（丸めない・弾かない）★", () => {
    expect(precheckMinutesFromCode(-1)).toBe(-1);
  });

  it("★本清掃コードだけが null で、それ以外は入力と同じ値を返す（プロパティテスト）★", () => {
    for (let code = 0; code <= 255; code += 1) {
      const result = precheckMinutesFromCode(code);
      if (code === CLEAN_TIMETABLE_PRECHECK.main) {
        expect(result).toBeNull();
      } else {
        expect(result).toBe(code);
      }
    }
  });
});

describe("statusColumnsFromPrecheckRows", () => {
  // ★期待値表（D-178。ユーザー判断 2026-10-16「案1」）★
  //   列の割り当ては ★分数の一致ではなく順番★。本清掃に付く事前チェックの予定（`precheck` の生値）を
  //   分数の大きい順（＝早い順）に並べ、1つ目を列1（`status_1h`）、2つ目を列2（`status_30m`）に入れる。
  //   E = 何らかのエラー1件（error_type は生値）/ S = ROBOT_STATUS_STANDBY（Standby.）
  //   ★null ＝ その回の事前チェックの予定行が無い★（ユーザー指示 2026-10-07「事前チェックがないものは-にしてください」。D-171）。
  //   ★Map にキーがある ＝ 予定行がある★（エラーが無ければ空配列で入る。`lib/db/index.ts` の詰め替え）
  //   ★分数はテストに直書きする★ — 本題は「設定（`PRECHECK_MINUTES_BEFORE`）と無関係に順番で割り当てる」ことなので、
  //   設定から読むと設定を変えたときに表の意味が変わる
  const E1: RobotErrorLike = { error_type: "EmergencyStop", errored_at: null };
  const E1b: RobotErrorLike = {
    error_type: "LostLocalization",
    errored_at: new Date("2026-08-27T01:10:00+09:00"),
  };
  const E2: RobotErrorLike = { error_type: "オフライン", errored_at: null };
  const E3: RobotErrorLike = { error_type: "充電中", errored_at: null };

  /** Map を組む短縮形（挿入順は結果に影響しないことも P10 で確かめる） */
  const rows = (...entries: [number, RobotErrorLike[]][]) => new Map(entries);

  /**
   * ★期待値表 P1〜P11（D-178）★
   *
   * | #   | 本清掃に付く事前チェックの予定（分前） | エラー             | 期待 status（列1 / 列2）       | 期待 minutes   | dropped |
   * |-----|----------------------------------------|--------------------|--------------------------------|----------------|---------|
   * | P1  | 60・30                                 | なし               | S / S                          | 60 / 30        | []      |
   * | P2  | 60・10（設定と違う分数）               | なし               | S / S                          | 60 / 10        | []      |
   * | P3  | 30 だけ                                | EmergencyStop      | "EmergencyStop" / null         | 30 / null      | []      |
   * | P4  | 無し                                   | —                  | null / null                    | null / null    | []      |
   * | P5  | 1・2（旧コード値。★1分前・2分前★）      | 1:E1 / 2:E2        | E2 / E1（2分前が先）           | 2 / 1          | []      |
   * | P6  | 90・60・30（3つ。通常は起きない）       | 90:E3 / 60:E1 / 30:E2 | E1 / E2（開始に近い2つ）    | 60 / 30        | [90]    |
   * | P7  | 60・30                                 | 60:E1 / 30:なし    | E1 / S                         | 60 / 30        | []      |
   * | P8  | 60・30                                 | 60:[E1,E1b] / 30:なし | robotStatusFromErrors([E1,E1b]) / S | 60 / 30 | []    |
   * | P9  | 10 だけ（設定 [60,30] に無い）          | なし               | S / null                       | 10 / null      | []      |
   * | P10 | 30・60（Map への挿入順が逆）            | 30:E2 / 60:E1      | E1 / E2（挿入順でなく分数順）  | 60 / 30        | []      |
   * | P11 | 本清掃（0）のキーが混ざる               | 0:E3 / 60:なし     | S / null（0 は事前チェックでない） | 60 / null  | []      |
   */
  describe("期待値表 P1〜P11（順番で割り当てる。D-178）", () => {
    it("P1: 60・30 でエラーが無ければ両方 Standby.・60 / 30", () => {
      const r = statusColumnsFromPrecheckRows(rows([60, []], [30, []]));
      expect(r.status).toEqual([STANDBY, STANDBY]);
      expect(r.minutes).toEqual([60, 30]);
      expect(r.dropped).toEqual([]);
    });

    it("★P2: 60・10（設定と違う分数）でも10分前の結果を落とさず2列目に出す★", () => {
      const r = statusColumnsFromPrecheckRows(rows([60, []], [10, [E1]]));
      expect(r.status).toEqual([STANDBY, E1.error_type]);
      expect(r.minutes).toEqual([60, 10]);
      expect(r.dropped).toEqual([]);
    });

    it("★P3: 30分前だけなら、分数によらず1列目に入り2列目は null★", () => {
      const r = statusColumnsFromPrecheckRows(rows([30, [E1]]));
      expect(r.status).toEqual([E1.error_type, null]);
      expect(r.minutes).toEqual([30, null]);
      expect(r.dropped).toEqual([]);
    });

    it("P4: 事前チェックの予定が無ければ両方 null（Standby. にしない。D-171）", () => {
      const r = statusColumnsFromPrecheckRows(new Map());
      expect(r.status).toEqual([null, null]);
      expect(r.minutes).toEqual([null, null]);
      expect(r.dropped).toEqual([]);
    });

    it("★P5: 旧コード値 1・2 は1分前・2分前として扱う（2分前が1回目。読み替えない。A5-3）★", () => {
      // ★意図した仕様変更（D-178）★: 以前は設定 [60,30] と一致しないため両方 null だった
      const r = statusColumnsFromPrecheckRows(rows([1, [E1]], [2, [E2]]));
      expect(r.status).toEqual([E2.error_type, E1.error_type]);
      expect(r.minutes).toEqual([2, 1]);
      expect(r.dropped).toEqual([]);
    });

    it("★P6: 3つあれば開始に近い2つ（60・30）を残し、90 を dropped で返す★", () => {
      const r = statusColumnsFromPrecheckRows(rows([90, [E3]], [60, [E1]], [30, [E2]]));
      expect(r.status).toEqual([E1.error_type, E2.error_type]);
      expect(r.minutes).toEqual([60, 30]);
      expect(r.dropped).toEqual([90]);
    });

    it("P7: 1回目だけ異常・2回目は予定ありエラー無し → E1 / Standby.", () => {
      const r = statusColumnsFromPrecheckRows(rows([60, [E1]], [30, []]));
      expect(r.status).toEqual([E1.error_type, STANDBY]);
      expect(r.minutes).toEqual([60, 30]);
    });

    it("P8: 複数エラーの畳み方は robotStatusFromErrors() に委ねる", () => {
      const r = statusColumnsFromPrecheckRows(rows([60, [E1, E1b]], [30, []]));
      expect(r.status).toEqual([robotStatusFromErrors([E1, E1b]), STANDBY]);
    });

    it("P9: 設定に無い分数（10分前）だけでも1列目に出す（旧: 両方 null）", () => {
      const r = statusColumnsFromPrecheckRows(rows([10, []]));
      expect(r.status).toEqual([STANDBY, null]);
      expect(r.minutes).toEqual([10, null]);
    });

    it("P10: Map への挿入順ではなく分数の大きい順に並べる（列が入れ替わらない）", () => {
      const r = statusColumnsFromPrecheckRows(rows([30, [E2]], [60, [E1]]));
      expect(r.status).toEqual([E1.error_type, E2.error_type]);
      expect(r.minutes).toEqual([60, 30]);
    });

    it("P11: 本清掃（CLEAN_TIMETABLE_PRECHECK.main）のキーは事前チェックとして数えない", () => {
      const r = statusColumnsFromPrecheckRows(
        rows([CLEAN_TIMETABLE_PRECHECK.main, [E3]], [60, []])
      );
      expect(r.status).toEqual([STANDBY, null]);
      expect(r.minutes).toEqual([60, null]);
      expect(r.dropped).toEqual([]);
    });
  });

  it("★不変条件: status が null のときだけ minutes も null（全ケース）★", () => {
    const cases = [
      new Map(),
      rows([30, []]),
      rows([60, []], [10, [E1]]),
      rows([90, []], [60, []], [30, [E2]]),
    ];
    for (const c of cases) {
      const r = statusColumnsFromPrecheckRows(c);
      for (const i of [0, 1] as const) {
        expect(r.status[i] === null).toBe(r.minutes[i] === null);
      }
    }
  });
});

describe("resolutionStatusFromRobotErrorStatus", () => {
  // 期待値表: 0 → unresolved / 1 → in_progress / 2 → resolved / それ以外 → 例外
  it("0/1/2 を3値に変換する", () => {
    expect(resolutionStatusFromRobotErrorStatus(0)).toBe("unresolved");
    expect(resolutionStatusFromRobotErrorStatus(1)).toBe("in_progress");
    expect(resolutionStatusFromRobotErrorStatus(2)).toBe("resolved");
  });

  it("想定外の値は例外にする（静かに既定値へ倒さない）", () => {
    // UnsignedTinyInt は 0〜255 を許すため 3以上が入りうる
    expect(() => resolutionStatusFromRobotErrorStatus(3)).toThrow(
      /想定外の値/
    );
    expect(() => resolutionStatusFromRobotErrorStatus(255)).toThrow(/想定外の値/);
    expect(() => resolutionStatusFromRobotErrorStatus(-1)).toThrow(/想定外の値/);
  });
});

describe("robotErrorStatusFromResolutionStatus", () => {
  it("3値を 0/1/2 に戻す（seed 用の逆変換）", () => {
    expect(robotErrorStatusFromResolutionStatus("unresolved")).toBe(0);
    expect(robotErrorStatusFromResolutionStatus("in_progress")).toBe(1);
    expect(robotErrorStatusFromResolutionStatus("resolved")).toBe(2);
  });

  it("正変換と逆変換が往復する", () => {
    const all: PrecheckStatus[] = ["unresolved", "in_progress", "resolved"];
    for (const status of all) {
      expect(
        resolutionStatusFromRobotErrorStatus(
          robotErrorStatusFromResolutionStatus(status)
        )
      ).toBe(status);
    }
  });
});

describe("detectedAtFromError", () => {
  const errored = new Date("2026-08-27T01:00:00+09:00");
  const created = new Date("2026-08-27T01:05:00+09:00");

  it("errored_at があればそれを使う", () => {
    expect(detectedAtFromError({ errored_at: errored, created_at: created })).toBe(
      errored
    );
  });

  it("errored_at が null のときだけ created_at に落とす", () => {
    expect(detectedAtFromError({ errored_at: null, created_at: created })).toBe(
      created
    );
  });
});

/**
 * `precheckItemFromErrorRow()`（指示書AT 段階4-1）— `prechecks()`（一覧）と `precheckItemById()`（PATCH）が
 * 共有する行の組み立て。★`prechecks()` が以前インラインで作っていた行と同じ形★になること
 * （一覧側の回帰は `app/api/prechecks/__tests__/route.test.ts` が無修正で通ることで見ている）。
 */
describe("precheckItemFromErrorRow", () => {
  /** DB の壁時計値としての `Date`（D-115。他のテストと同じヘルパー） */
  function dbWallClock(jstIso: string): Date {
    return new Date(jstIso.replace("+09:00", "Z"));
  }

  const report = {
    error_id: "RER-1001-1",
    serial_number: "8111R5628060011",
    facility_id: BigInt(3),
    facility: { facility_name: "バリューマート北町店" },
    error_type: "EmergencyStop",
    errored_at: dbWallClock("2026-09-10T01:05:00+09:00"),
    created_at: dbWallClock("2026-09-10T01:06:00+09:00"),
    status: 1,
    responded_at: dbWallClock("2026-09-10T07:00:00+09:00"),
    resolved_at: null,
    comment: "現場に連絡済み",
  };

  it("一覧と同じ形の行を作る（id は error_id のまま・日時は DB の壁時計値そのまま）", () => {
    const item = precheckItemFromErrorRow(
      report,
      PRECHECK_MINUTES_BEFORE[0],
      { start_at: dbWallClock("2026-09-10T02:00:00+09:00") },
      // ★親の本清掃にレポート行が無い★ → `main_run_succeeded: false`（D-171 の M4）
      undefined
    );
    expect(item).toEqual({
      id: "RER-1001-1",
      severity: "precheck",
      precheck_minutes: PRECHECK_MINUTES_BEFORE[0],
      serial_number: "8111R5628060011",
      facility_id: 3,
      facility_name: "バリューマート北町店",
      // ★親の本清掃行の start_at★（事前チェック行の時刻ではない）
      scheduled_at: "2026-09-10T02:00:00+09:00",
      robot_status: robotStatusFromErrors([
        { error_type: "EmergencyStop", errored_at: report.errored_at },
      ]),
      detected_at: "2026-09-10T01:05:00+09:00",
      resolution_status: "in_progress",
      responded_at: "2026-09-10T07:00:00+09:00",
      resolved_at: null,
      note: "現場に連絡済み",
      battery_level: null,
      main_run_succeeded: false,
    });
  });

  it("errored_at が null なら created_at を検知時刻にする / 解消日時を変換する", () => {
    const item = precheckItemFromErrorRow(
      { ...report, errored_at: null, status: 2, resolved_at: dbWallClock("2026-09-10T08:00:00+09:00") },
      PRECHECK_MINUTES_BEFORE[1],
      { start_at: dbWallClock("2026-09-10T02:00:00+09:00") },
      undefined
    );
    expect(item.detected_at).toBe("2026-09-10T01:06:00+09:00");
    expect(item.resolution_status).toBe("resolved");
    expect(item.resolved_at).toBe("2026-09-10T08:00:00+09:00");
  });

  it("★親の本清掃のレポートから main_run_succeeded を決める★（`mainRunSucceededFromReport()` の1本。D-171）", () => {
    const start = { start_at: dbWallClock("2026-09-10T02:00:00+09:00") };
    const ok = precheckItemFromErrorRow(report, PRECHECK_MINUTES_BEFORE[0], start, {
      clean_area: 95,
      task_area: 100,
      clean_ratio: 0,
    });
    expect(ok.main_run_succeeded).toBe(true);
    // ★対応状況は変えない★（本清掃が成功しても未解消・対応中のまま。D-171）
    expect(ok.resolution_status).toBe("in_progress");

    const ng = precheckItemFromErrorRow(report, PRECHECK_MINUTES_BEFORE[0], start, {
      clean_area: 50,
      task_area: 100,
      clean_ratio: 0,
    });
    expect(ng.main_run_succeeded).toBe(false);
  });
});

/**
 * ★`mainRunSucceededFromReport()` の期待値表（M1〜M6。ユーザー指示 2026-10-07。D-171）★
 *
 * ★成功 ＝ 親の本清掃にレポート行があり、完了率の判定が ◎○★（`SUCCESS_GRADES`。ダッシュボードの「成功」と同じ定義）。
 * ★完了率・等級は既存の関数で決める★（`completionRateFromReport()` → `judgeGrade()`）。ここで割り算や閾値を書かない。
 * ★`clean_ratio` 列は使わない★（D-155）ので、わざと逆向きの値を入れて読んでいないことも確かめる。
 */
describe("mainRunSucceededFromReport（D-171 期待値表 M1〜M6）", () => {
  const row = (clean_area: number, task_area: number) => ({
    clean_area,
    task_area,
    // ★完了率には使わない列★ — 読んでいたら M1/M2 と M3/M5 の答えが逆になる
    clean_ratio: clean_area >= task_area * 0.7 ? 0 : 100,
  });

  it("M1: レポートあり・完了率 95（◎）→ true", () => {
    expect(judgeGrade(95)).toBe("◎");
    expect(mainRunSucceededFromReport(row(95, 100))).toBe(true);
  });

  it("M2: レポートあり・完了率 72（○）→ true", () => {
    expect(judgeGrade(72)).toBe("○");
    expect(mainRunSucceededFromReport(row(72, 100))).toBe(true);
  });

  it("M3: レポートあり・完了率 50（△）→ false", () => {
    expect(judgeGrade(50)).toBe("△");
    expect(mainRunSucceededFromReport(row(50, 100))).toBe(false);
  });

  it("M4: レポートなし → false", () => {
    expect(mainRunSucceededFromReport(undefined)).toBe(false);
  });

  it("M5: レポートあり・task_area 0（完了率 0 → ×）→ false", () => {
    // ★`task_area = 0` は 0%（P-5=B）★ — 清掃面積が大きくても成功にしない
    expect(mainRunSucceededFromReport(row(500, 0))).toBe(false);
  });

  it("M6: 出発失敗の行（severity: launch_failed）→ false（fixtures。Prisma 版は route.test.ts）", () => {
    const launchFailed = fixturePrechecks.filter((p) => p.severity === "launch_failed");
    expect(launchFailed.length).toBeGreaterThan(0);
    for (const p of launchFailed) expect(p.main_run_succeeded).toBe(false);
  });

  it("fixtures: id 6（出発成功）は true、id 7（完了率62% ＝ △）は false", () => {
    const byId = (id: string) => fixturePrechecks.find((p) => p.id === id);
    expect(byId("6")?.main_run_succeeded).toBe(true);
    expect(byId("7")?.main_run_succeeded).toBe(false);
    // ★true は id 6 の1件だけ★（ほかは false。D-171）
    expect(fixturePrechecks.filter((p) => p.main_run_succeeded).map((p) => p.id)).toEqual(["6"]);
  });
});
