/**
 * パスパラメータ検証（`lib/api/path-params.ts`）のテスト。
 *
 * ★期待値表を先に作ってから実装している★（`CLAUDE.md` §9.4 / 指示書AE 段階2-2-4）。
 * ★`Number.parseInt` の前方一致で黙って通っていた入力（`"-5"` / `"12abc"` / `"1.9"` / `"1e9"`）を
 *   ここで固定する★ — 実装を「速いから」と `parseInt` に戻すと、このテストが落ちる。
 */
import { describe, expect, it } from "vitest";
import {
  parsePathErrorId,
  parsePathId,
  parsePathReportId,
  parsePathSerialNumber,
} from "@/lib/api/path-params";
import {
  PATH_ERROR_ID_MAX_LENGTH,
  PATH_REPORT_ID_MAX_LENGTH,
  PATH_SERIAL_NUMBER_MAX_LENGTH,
} from "@/lib/config";

/** 失敗ケースの共通の期待: 422 / validation_failed */
function expectRejected(result: ReturnType<typeof parsePathId>) {
  expect(result.ok).toBe(false);
  if (result.ok) return;
  expect(result.status).toBe(422);
  expect(result.error.error.code).toBe("validation_failed");
}

describe("parsePathId", () => {
  describe("通る入力", () => {
    it.each([
      ["1", 1],
      ["538", 538],
      // 上限ちょうど（安全整数の最大値）は通る。ここが落ちると境界の向きが逆
      ["9007199254740991", 9007199254740991],
    ])("%s は %i として通る", (input, expected) => {
      const result = parsePathId(input);
      expect(result.ok).toBe(true);
      if (!result.ok) return;
      expect(result.query).toBe(expected);
    });
  });

  describe("422 で弾く入力", () => {
    it.each([
      ["0", "IDは1以上（0は存在しない）"],
      ["-5", "負数。parseInt は -5 として通してしまう"],
      ["1.9", "非整数。parseInt は 1 に切り捨てて通してしまう"],
      ["12abc", "ゴミ付き。parseInt は 12 として通してしまう"],
      ["1e9", "指数表記。parseInt は 1 として通してしまう"],
      ["", "空文字"],
      ["9007199254740993", "安全整数超（Number() で桁が落ちる）"],
      ["１", "全角数字（JS の \\d は ASCII のみ）"],
      [" 1", "前に空白。トリムして受け入れない"],
      ["1 ", "後ろに空白。トリムして受け入れない"],
      ["1\n", "末尾改行。$ は m フラグ無しなので許さない"],
      ["+1", "符号付き"],
      ["0x10", "16進表記"],
      ["abc", "数字を含まない"],
    ])("%s は422（%s）", (input) => {
      expectRejected(parsePathId(input));
    });
  });

  it("エラー文言と details に入力値をエコーしない", () => {
    const attack = "99999999-echo-me";
    const result = parsePathId(attack);
    expect(result.ok).toBe(false);
    if (result.ok) return;
    const serialized = JSON.stringify(result.error);
    expect(serialized).not.toContain(attack);
    expect(serialized).not.toContain("echo-me");
  });

  it("details は details キーを落とさない（API契約上 details は常に存在する）", () => {
    const result = parsePathId("0");
    expect(result.ok).toBe(false);
    if (result.ok) return;
    expect(result.error.error.details).not.toBeNull();
  });
});

describe("parsePathSerialNumber", () => {
  it("通常のシリアルは通る（fixtures 形式・YAML の example 形式の両方）", () => {
    for (const input of ["8111R5628060011", "DS-001"]) {
      const result = parsePathSerialNumber(input);
      expect(result.ok).toBe(true);
      if (!result.ok) return;
      expect(result.query).toBe(input);
    }
  });

  it("上限ちょうどの長さは通る", () => {
    const input = "A".repeat(PATH_SERIAL_NUMBER_MAX_LENGTH);
    const result = parsePathSerialNumber(input);
    expect(result.ok).toBe(true);
  });

  it("上限+1 の長さは422", () => {
    const result = parsePathSerialNumber("A".repeat(PATH_SERIAL_NUMBER_MAX_LENGTH + 1));
    expect(result.ok).toBe(false);
    if (result.ok) return;
    expect(result.status).toBe(422);
    expect(result.error.error.code).toBe("validation_failed");
  });

  it("空文字は422", () => {
    const result = parsePathSerialNumber("");
    expect(result.ok).toBe(false);
    if (result.ok) return;
    expect(result.status).toBe(422);
  });

  it("★文字種は絞らない★ — 記号や日本語を含んでいても長さ以内なら通る", () => {
    // 実データの形式は TeamWorks 側で決まり、DFA側で未確定（CLAUDE.md §7）。
    // ここが落ちるように「英数字のみ」等を足すと、未知の形式を推測で弾くことになる
    for (const input of ["DS_001", "ds-001", "制御盤1", "8111R5628060011/"]) {
      expect(parsePathSerialNumber(input).ok).toBe(true);
    }
  });

  it("エラー文言と details に入力値をエコーしない", () => {
    const attack = `${"A".repeat(PATH_SERIAL_NUMBER_MAX_LENGTH)}echo-me`;
    const result = parsePathSerialNumber(attack);
    expect(result.ok).toBe(false);
    if (result.ok) return;
    expect(JSON.stringify(result.error)).not.toContain("echo-me");
  });
});

/**
 * `parsePathErrorId()`（指示書AT 段階1）— `robot_error_reports.error_id` は `varchar(50)`。
 * ★`parsePathSerialNumber()` と同じ作法★（空文字と上限超だけ 422、文字種は絞らない）。
 */
describe("parsePathErrorId", () => {
  it("上限は varchar(50) と同じ 50", () => {
    expect(PATH_ERROR_ID_MAX_LENGTH).toBe(50);
  });

  it("50文字ちょうどは通る（値はそのまま。Number() を通さない）", () => {
    const input = "A".repeat(50);
    const result = parsePathErrorId(input);
    expect(result.ok).toBe(true);
    if (!result.ok) return;
    expect(result.query).toBe(input);
  });

  it("51文字は422", () => {
    const result = parsePathErrorId("A".repeat(51));
    expect(result.ok).toBe(false);
    if (result.ok) return;
    expect(result.status).toBe(422);
    expect(result.error.error.code).toBe("validation_failed");
  });

  it("空文字は422", () => {
    const result = parsePathErrorId("");
    expect(result.ok).toBe(false);
    if (result.ok) return;
    expect(result.status).toBe(422);
  });

  it("★文字種は絞らない★ — 記号・日本語・数字だけの値も長さ以内なら通る", () => {
    for (const input of ["RER-1001-1", "1", "007", "err_01/x", "エラー1", " 1"]) {
      const result = parsePathErrorId(input);
      expect(result.ok).toBe(true);
      if (!result.ok) return;
      // ★数値化しない★（"007" が "7" にならない）
      expect(result.query).toBe(input);
    }
  });

  it("エラー文言と details に入力値をエコーしない", () => {
    const attack = `${"A".repeat(50)}echo-me`;
    const result = parsePathErrorId(attack);
    expect(result.ok).toBe(false);
    if (result.ok) return;
    expect(JSON.stringify(result.error)).not.toContain("echo-me");
  });
});

/** 表P — `parsePathReportId()`（指示書BI §4） */
describe("parsePathReportId", () => {
  const details422 = { report_id: [`${PATH_REPORT_ID_MAX_LENGTH}文字以内で指定してください`] };

  it("P-1: 空文字は422（details を丸ごと）", () => {
    const result = parsePathReportId("");
    expect(result.ok).toBe(false);
    if (result.ok) return;
    expect(result.status).toBe(422);
    expect(result.error.error.details).toEqual(details422);
  });

  it("P-2: 19桁の数字文字列はそのまま通る", () => {
    const input = "9200000000000000002";
    const result = parsePathReportId(input);
    expect(result.ok).toBe(true);
    if (!result.ok) return;
    expect(result.query).toBe(input);
  });

  it("P-3: 20文字は通る", () => {
    const input = "1".repeat(20);
    const result = parsePathReportId(input);
    expect(result.ok).toBe(true);
    if (!result.ok) return;
    expect(result.query).toBe(input);
  });

  it("P-4: 21文字は422（本文に入力値が出ない）", () => {
    const input = "1".repeat(21);
    const result = parsePathReportId(input);
    expect(result.ok).toBe(false);
    if (result.ok) return;
    expect(result.status).toBe(422);
    expect(result.error.error.code).toBe("validation_failed");
    expect(result.error.error.details).toEqual(details422);
    expect(JSON.stringify(result.error)).not.toContain(input);
  });

  it("P-5: abc は通る（文字種は絞らない）", () => {
    const result = parsePathReportId("abc");
    expect(result.ok).toBe(true);
    if (!result.ok) return;
    expect(result.query).toBe("abc");
  });
});
