import { describe, expect, it } from "vitest";

import { facilityIdsMatchingName } from "@/lib/facilities/resolve-by-name";
import { includesFoldName } from "@/lib/format/search-text";
import type { FacilityNameRef } from "@/lib/types";

/**
 * 指示書AC 段階2 ★D-6 / C-1★ — 施設名 → 施設ID の解決（★純粋関数の部分★）。
 *
 * ★★ここは「解決関数そのもの」のテストです★★。
 *   ★★これだけでは不十分★★ — ★ルートが `facility_ids` を渡し忘れても赤くなりません★。
 *   ★API ルートから `db` までを通した経路のテストは
 *   `app/api/clean-logs/__tests__/route.test.ts`（期待値表 #6d / #6e）★。
 *
 * ★照合規則は `lib/format/search-text.ts` の `includesFoldName` が正★（指示書AG D-1 / D-5）。
 * ★★このファイルに照合規則を書き下さないこと★★ — 規則が2箇所に散る。
 */

/**
 * ★実在の店名を使わないための架空名★（`CLAUDE.md` §7 / `docs/fixtures.md`）。
 * ★中点入りの店名★を再現するため、`ドン` ではなく `ドソ` にしてある
 * （記号ゆれの検証が目的で、実在名である必要は無い）。
 */
const REFS: readonly FacilityNameRef[] = [
  { id: 1, facility_name: "ドソ・キホーテ戸塚" },
  { id: 2, facility_name: "ドソ・キホーテ四街道" },
  { id: 3, facility_name: "メガドソ・キホーテ 港南" },
  { id: 4, facility_name: "フレッシュタウン大和店" },
  { id: 5, facility_name: "バリュー－マート北町店" },
];

describe("facilityIdsMatchingName（指示書AC 段階2 D-6）", () => {
  it("★★空文字は `null`（＝施設で絞らない）★★。★`[]` を返さないこと★", () => {
    // ★`[]` は「0件ヒット」の意味★ — 空文字で `[]` を返すと、施設名を入力していないのに
    //   一覧が空になる（罠1 / `CleanLogDbQuery.facility_ids` の型コメント）
    expect(facilityIdsMatchingName(REFS, "")).toBeNull();
  });

  it("★★1件も当たらなければ `[]`（＝0件ヒット）★★。★`null` を返さないこと★", () => {
    // ★`null` を返すと「条件なし」になり、★0件のはずが全件表示に化ける★
    expect(facilityIdsMatchingName(REFS, "存在しない店名")).toEqual([]);
  });

  it("★#6d 記号違いで当たる★（中点を無視する。指示書AG の本命）", () => {
    expect(facilityIdsMatchingName(REFS, "ドソキホーテ")).toEqual([1, 2, 3]);
  });

  it("★部分一致なので複数施設に当たりうる★（だから戻り値は配列）", () => {
    expect(facilityIdsMatchingName(REFS, "ドソキ")).toEqual([1, 2, 3]);
    expect(facilityIdsMatchingName(REFS, "戸塚")).toEqual([1]);
  });

  it("空白・ハイフン類も無視される（★規則は `includesFoldName` の写しではなく委譲★）", () => {
    expect(facilityIdsMatchingName(REFS, "ドソキホーテ港南")).toEqual([3]);
    expect(facilityIdsMatchingName(REFS, "バリューマート")).toEqual([5]);
    // ★委譲していることの確認★ — 同じ判定が `includesFoldName` 単体でも成り立つ
    expect(includesFoldName("バリュー－マート北町店", "バリューマート")).toBe(true);
  });

  it("★`contains` の罠（`%` をエスケープしない）が発生しない★", () => {
    // ★Prisma の `contains: "%"` は施設が全部ヒットする（指示書AC C-1 の実測）★。
    //   ★本案は施設名を SQL に渡さないので、JS の `.includes()` の挙動がそのまま出る★
    expect(facilityIdsMatchingName(REFS, "%")).toEqual([]);
  });

  // ★★2026-09-14 に反転させた★★（ユーザー判断: 記号のみ検索は弾く。案A）
  //   反転前の期待値は ★`[1, 2, 3, 4, 5]`（全施設 = 絞り込み無しと同じ）★ だった。
  //   `foldName("・") === ""` → `includes("")` が常に true、という `foldName` の帰結を
  //   そのまま通していたため。★検索欄に何か入力しているのに全件が出る★のは
  //   「絞り込めている」と誤認させるので、★`includesFoldName` 側で false に倒した★。
  it("★記号だけの入力は1件も当たらない★（`[]` = 0件ヒット。2026-09-14 に反転）", () => {
    // ★`null`（条件なし）ではなく `[]`（0件ヒット）★ —
    //   `null` を返すと `facility_ids` の条件自体が消えて、★0件が全件表示に化ける★（罠1）
    expect(facilityIdsMatchingName(REFS, "・")).toEqual([]);
    expect(facilityIdsMatchingName(REFS, "_")).toEqual([]);
    // ★空文字（未指定）は反転していない★ — 素通しのまま（`null`）
    expect(facilityIdsMatchingName(REFS, "")).toBeNull();
  });

  it("★#6e 論理削除された施設も解決対象に含まれる★（罠7）", () => {
    // ★解決用の読み取りが `deleted_at` で絞られていなければ、削除済みの施設も
    //   `refs` に入ってくる（`loadFacilityNameIndex()`）。ここはそれを前提に解決する★
    const withDeleted: FacilityNameRef[] = [
      ...REFS,
      { id: 99, facility_name: "ドソ・キホーテ旧店舗" },
    ];
    expect(facilityIdsMatchingName(withDeleted, "旧店舗")).toEqual([99]);
    expect(facilityIdsMatchingName(withDeleted, "ドソキホーテ")).toContain(99);
  });

  it("施設マスタが空でも例外にならない（`[]` を返す）", () => {
    expect(facilityIdsMatchingName([], "何か")).toEqual([]);
    expect(facilityIdsMatchingName([], "")).toBeNull();
  });

  it("★入力の配列を壊さない（純粋関数）★", () => {
    const before = [...REFS];
    facilityIdsMatchingName(REFS, "ドソキ");
    expect(REFS).toEqual(before);
  });
});
