/**
 * 指示書AY §3 表1（`lib/ds-logs/query.ts` の `queryDeviceLogs()`）。
 * ブリーフ §5 と監督判断 第2ラウンドから本書で具体化。★P1・P2 の行は §11 Q14・Q15 の採用で確定★。
 *
 * ★表1 は純粋関数の入力なので架空の名前のまま★（§3 冒頭・§11-4 M6。実機の種類・name の形は fixtures でだけ使う）。
 *
 * types（制御盤から来た順）= app「アプリ」/ comm「通信」/ sys「システム」。now = 2026-10-19T12:00:00+09:00
 *
 * | file_id | name                   | type | size | modified_at               | 備考               |
 * | f1      | app_20261019_1100.log  | app  | 1000 | 2026-10-19T11:00:00+09:00 | f3 と同時刻        |
 * | f2      | app_20261019_0900.log  | app  | 2000 | 2026-10-19T09:00:00+09:00 |                    |
 * | f3      | comm_20261019_1100.log | comm | 3000 | 2026-10-19T11:00:00+09:00 | f1 と同時刻        |
 * | f4      | comm_20261018_1200.log | comm | 4000 | 2026-10-18T12:00:00+09:00 | ちょうど24時間前   |
 * | f5      | sys_20261019_1130.log  | sys  | 5000 | 2026-10-19T11:30:00+09:00 |                    |
 * | f6      | sys_20261018_1159.log  | sys  | 6000 | 2026-10-18T11:59:59+09:00 | 24時間前の1秒前    |
 * | f7      | app_20261019_0200.log  | app  | 7000 | 2026-10-18T17:00:00Z      | = 10/19 02:00 JST  |
 * | f8      | app_20261019_1200.log  | app  | 8000 | 2026-10-19T12:00:00+09:00 | ちょうど now       |
 *
 * | #   | 入力                                              | 期待（file_id の並び）              | total |
 * | Q1  | 指定なし（sort=date_desc）                        | f8, f5, f1, f3, f2, f7, f4, f6       | 8 |
 * | Q2  | types=app                                         | f8, f1, f2, f7                       | 4 |
 * | Q3  | types=app,sys                                     | f8, f5, f1, f2, f7, f6               | 6 |
 * | Q4  | types=xyz                                         | （空）                               | 0 |
 * | Q5  | types=app,xyz                                     | f8, f1, f2, f7                       | 4 |
 * | Q6  | from=2026-10-19T11:00:00+09:00                    | f8, f5, f1, f3                       | 4 |（ちょうど from は入る）
 * | Q7  | to=2026-10-19T11:00:00+09:00                      | f2, f7, f4, f6                       | 4 |（ちょうど to は入らない）
 * | Q8  | from=10-18T12:00+09:00 & to=10-19T12:00+09:00     | f5, f1, f3, f2, f7, f4               | 6 |
 * | Q9  | from=10-19T02:00:00+09:00 & to=10-19T02:00:01+09:00 | f7                                 | 1 |（比較は瞬間）
 * | Q10 | from=2026-10-19T02:30:00Z                         | f8, f5                               | 2 |
 * | Q11 | sort=type_date_desc                               | f8, f1, f2, f7, f3, f4, f5, f6       | 8 |
 * | Q12 | sort=type_date_desc & types=comm,app              | f8, f1, f2, f7, f3, f4               | 6 |（制御盤の types の順）
 * | Q13 | from=2026-10-18T12:00:00+09:00（直近24時間）      | f8, f5, f1, f3, f2, f7, f4           | 7 |
 * | Q14 | Q1 を perPage=3, page=1                           | f8, f5, f1                           | 8 |
 * | Q15 | Q1 を perPage=3, page=3                           | f4, f6                               | 8 |
 * | Q16 | Q1 を perPage=3, page=4                           | （空）                               | 8 |
 * | Q17 | 出力の modified_at                                | f7 は 2026-10-19T02:00:00+09:00、他は入力どおり | — |
 */
import { describe, expect, it } from "vitest";
import { deviceLogTypesParam, queryDeviceLogs, type DeviceLogQuery } from "@/lib/ds-logs/query";
import type { DsLogFile, DsLogType } from "@/lib/types";

const TYPES: readonly DsLogType[] = [
  { key: "app", label: "アプリ" },
  { key: "comm", label: "通信" },
  { key: "sys", label: "システム" },
];

const ITEMS: readonly DsLogFile[] = [
  { file_id: "f1", name: "app_20261019_1100.log", type: "app", size_bytes: 1000, modified_at: "2026-10-19T11:00:00+09:00" },
  { file_id: "f2", name: "app_20261019_0900.log", type: "app", size_bytes: 2000, modified_at: "2026-10-19T09:00:00+09:00" },
  { file_id: "f3", name: "comm_20261019_1100.log", type: "comm", size_bytes: 3000, modified_at: "2026-10-19T11:00:00+09:00" },
  { file_id: "f4", name: "comm_20261018_1200.log", type: "comm", size_bytes: 4000, modified_at: "2026-10-18T12:00:00+09:00" },
  { file_id: "f5", name: "sys_20261019_1130.log", type: "sys", size_bytes: 5000, modified_at: "2026-10-19T11:30:00+09:00" },
  { file_id: "f6", name: "sys_20261018_1159.log", type: "sys", size_bytes: 6000, modified_at: "2026-10-18T11:59:59+09:00" },
  { file_id: "f7", name: "app_20261019_0200.log", type: "app", size_bytes: 7000, modified_at: "2026-10-18T17:00:00Z" },
  { file_id: "f8", name: "app_20261019_1200.log", type: "app", size_bytes: 8000, modified_at: "2026-10-19T12:00:00+09:00" },
];

/** 全件が1ページに収まる大きさ（表1 の Q1〜Q13 はページ分けを見ない） */
const ALL = 1000;

function q(overrides: Partial<DeviceLogQuery> = {}): DeviceLogQuery {
  return {
    types: null,
    from: null,
    to: null,
    sort: "date_desc",
    page: 1,
    perPage: ALL,
    ...overrides,
  };
}

const at = (iso: string): number => Date.parse(iso);

describe("queryDeviceLogs() — 表1", () => {
  it.each<[string, DeviceLogQuery, string[], number]>([
    ["Q1 指定なし（新しい順・同時刻は name 昇順）", q(), ["f8", "f5", "f1", "f3", "f2", "f7", "f4", "f6"], 8],
    ["Q2 types=app", q({ types: ["app"] }), ["f8", "f1", "f2", "f7"], 4],
    ["Q3 types=app,sys", q({ types: ["app", "sys"] }), ["f8", "f5", "f1", "f2", "f7", "f6"], 6],
    ["Q4 types=xyz（未知のキーは0件。D23）", q({ types: ["xyz"] }), [], 0],
    ["Q5 types=app,xyz（D23）", q({ types: ["app", "xyz"] }), ["f8", "f1", "f2", "f7"], 4],
    ["Q6 ちょうど from は入る", q({ from: at("2026-10-19T11:00:00+09:00") }), ["f8", "f5", "f1", "f3"], 4],
    ["Q7 ちょうど to は入らない", q({ to: at("2026-10-19T11:00:00+09:00") }), ["f2", "f7", "f4", "f6"], 4],
    [
      "Q8 f4 は入る・f8 は入らない・f6 は前",
      q({ from: at("2026-10-18T12:00:00+09:00"), to: at("2026-10-19T12:00:00+09:00") }),
      ["f5", "f1", "f3", "f2", "f7", "f4"],
      6,
    ],
    [
      "Q9 比較は瞬間（Date.parse）。文字列で比べない",
      q({ from: at("2026-10-19T02:00:00+09:00"), to: at("2026-10-19T02:00:01+09:00") }),
      ["f7"],
      1,
    ],
    ["Q10 from 側の Z も瞬間で", q({ from: at("2026-10-19T02:30:00Z") }), ["f8", "f5"], 2],
    [
      "Q11 type_date_desc（types 配列の順 → 新しい順 → name 昇順）",
      q({ sort: "type_date_desc" }),
      ["f8", "f1", "f2", "f7", "f3", "f4", "f5", "f6"],
      8,
    ],
    [
      "Q12 クエリの順ではなく制御盤の types の順",
      q({ sort: "type_date_desc", types: ["comm", "app"] }),
      ["f8", "f1", "f2", "f7", "f3", "f4"],
      6,
    ],
    [
      "Q13 直近24時間（f6 は入らない）",
      q({ from: at("2026-10-18T12:00:00+09:00") }),
      ["f8", "f5", "f1", "f3", "f2", "f7", "f4"],
      7,
    ],
    ["Q14 perPage=3, page=1", q({ perPage: 3, page: 1 }), ["f8", "f5", "f1"], 8],
    ["Q15 perPage=3, page=3（最終ページの端数）", q({ perPage: 3, page: 3 }), ["f4", "f6"], 8],
    ["Q16 perPage=3, page=4（範囲外は空配列・total は正しい）", q({ perPage: 3, page: 4 }), [], 8],
  ])("%s", (_label, query, expectedIds, expectedTotal) => {
    const result = queryDeviceLogs(TYPES, ITEMS, query);
    expect(result.items.map((i) => i.file_id)).toEqual(expectedIds);
    expect(result.total).toBe(expectedTotal);
  });

  it("Q17 出力の modified_at は toJstIso で JST に揃える（f7 だけ変わる）", () => {
    const result = queryDeviceLogs(TYPES, ITEMS, q());
    const byId = new Map(result.items.map((i) => [i.file_id, i.modified_at]));
    expect(byId.get("f7")).toBe("2026-10-19T02:00:00+09:00");
    for (const item of ITEMS) {
      if (item.file_id === "f7") continue;
      expect(byId.get(item.file_id)).toBe(item.modified_at);
    }
  });

  it("入力の配列を書き換えない", () => {
    const items = ITEMS.map((i) => ({ ...i }));
    const snapshot = JSON.stringify(items);
    queryDeviceLogs(TYPES, items, q({ sort: "type_date_desc" }));
    expect(JSON.stringify(items)).toBe(snapshot);
  });
});

describe("deviceLogTypesParam() — 指示書BJ 表Q", () => {
  it.each<[string, readonly string[], ReadonlySet<string> | null, string | null]>([
    ["Q1 selected null", ["a", "b", "c"], null, null],
    ["Q2 全種類", ["a", "b", "c"], new Set(["a", "b", "c"]), null],
    ["Q3 一部（known の順）", ["a", "b", "c"], new Set(["a", "b"]), "a,b"],
    ["Q4 飛び（known の順）", ["a", "b", "c"], new Set(["c", "a"]), "a,c"],
    ["Q5 件数同じだが known を含まない", ["a", "b"], new Set(["a", "x"]), "a,x"],
    ["Q6 種類が無い", [], new Set(), null],
  ])("%s", (_label, known, selected, expected) => {
    expect(deviceLogTypesParam(known, selected)).toBe(expected);
  });
});
