import { describe, expect, it } from "vitest";

import { filterCleanLogs, type CleanLogQuery } from "@/lib/clean-logs/query";
import { filterFixtureCleanLogs } from "@/lib/db/fixture-clean-logs";
import { cleanLogs, unscheduledCleanLogs } from "@/lib/fixtures";
import type { CleanLogDbQuery } from "@/lib/types";

/**
 * 指示書AC 段階2 ★D-5★ — fixtures 版も同じ引数を受け取り、同じ結果を返す。
 *
 * ★★`USE_FIXTURES` の true/false で件数が変わってはいけない★★（指示書AC 禁止事項6）。
 * ★★`null`（条件なし）と `[]`（0件ヒット）を同じ意味で扱わないこと★★（罠9 / 罠1）。
 *
 * ★fixtures の**既存の値**は変更していない★（禁止事項5）— ここは読むだけ。
 */

const ALL: CleanLogDbQuery = { id: null, date_from: null, date_to: null, facility_ids: null };

/** API ルート側の絞り込み（＝規則の「正」）で同じ条件を書いたもの */
function memoryQuery(partial: Partial<CleanLogQuery>): CleanLogQuery {
  return {
    dateFrom: null,
    dateTo: null,
    facilityName: "",
    model: "",
    serial: "",
    result: "all",
    // ★絞り込まない（指示書AK 段階2 で足った2つ。`null` = 条件なし）★
    groupId: null,
    customerId: null,
    page: 1,
    perPage: 50,
    ...partial,
  };
}

function jstDate(day: string): Date {
  return new Date(`${day}T00:00:00+09:00`);
}

/** fixtures の中でデータが載っている日（`at()` が相対日で作るので実行日から決める） */
const days = [...new Set(cleanLogs.map((r) => r.scheduled_at.slice(0, 10)))].sort();
const middleDay = days[Math.floor(days.length / 2)];

describe("filterFixtureCleanLogs（指示書AC 段階2 D-5）", () => {
  it("#1 範囲なし（全部 null）→ fixtures の全件", () => {
    expect(filterFixtureCleanLogs(cleanLogs, ALL)).toHaveLength(cleanLogs.length);
  });

  it("#2 日付範囲 → ★API ルート側の `filterCleanLogs()` と完全一致★", () => {
    const from = days[1];
    const to = days[3];
    const sql = filterFixtureCleanLogs(cleanLogs, {
      id: null,
      date_from: from,
      date_to: to,
      facility_ids: null,
    });
    const memory = filterCleanLogs(
      [...cleanLogs],
      memoryQuery({ dateFrom: jstDate(from), dateTo: jstDate(to) })
    );
    // ★件数だけでなく id の集合まで比べる★（指示書AC G-3）
    expect(sql.map((r) => r.id)).toEqual(memory.map((r) => r.id));
    expect(sql).toEqual(memory);
    expect(sql.length).toBeGreaterThan(0);
  });

  it("#3 `date_from = date_to` → その日の行だけ", () => {
    const rows = filterFixtureCleanLogs(cleanLogs, {
      id: null,
      date_from: middleDay,
      date_to: middleDay,
      facility_ids: null,
    });
    expect(rows.length).toBeGreaterThan(0);
    expect([...new Set(rows.map((r) => r.scheduled_at.slice(0, 10)))]).toEqual([middleDay]);
  });

  it("#3 上限はその日を**含む**（★翌日 00:00 未満★）", () => {
    const rows = filterFixtureCleanLogs(cleanLogs, {
      id: null,
      date_from: null,
      date_to: middleDay,
      facility_ids: null,
    });
    expect(rows.some((r) => r.scheduled_at.slice(0, 10) === middleDay)).toBe(true);
    expect(rows.every((r) => r.scheduled_at.slice(0, 10) <= middleDay)).toBe(true);
  });

  it("#4 `date_from` がデータの最古日より前 → #1 と同じ件数", () => {
    const rows = filterFixtureCleanLogs(cleanLogs, {
      id: null,
      date_from: "2000-01-01",
      date_to: null,
      facility_ids: null,
    });
    expect(rows).toHaveLength(cleanLogs.length);
  });

  it("#6 `facility_ids: [A, B]` → その2施設だけ（★メモリ絞り込みと一致★）", () => {
    const ids = [...new Set(cleanLogs.map((r) => r.facility_id))].sort((a, b) => a - b);
    const two = [ids[0], ids[1]];
    const rows = filterFixtureCleanLogs(cleanLogs, {
      id: null,
      date_from: null,
      date_to: null,
      facility_ids: two,
    });
    expect(rows).toEqual(cleanLogs.filter((r) => two.includes(r.facility_id)));
    expect(rows.length).toBeGreaterThan(0);
  });

  it("#6 1施設だけのケース", () => {
    const id = cleanLogs[0].facility_id;
    const rows = filterFixtureCleanLogs(cleanLogs, {
      id: null,
      date_from: null,
      date_to: null,
      facility_ids: [id],
    });
    expect(rows.every((r) => r.facility_id === id)).toBe(true);
    expect(rows.length).toBeGreaterThan(0);
  });

  it("★#6b★ `facility_ids: []` → ★★0件★★（★`in: undefined` に化けていないことの検出。罠1★）", () => {
    // ★★このテストが唯一の検出手段★★ —
    //   `[]` を「条件なし」と読んで全件返す実装にすると、Prisma 版（`in: []` = 0件）と
    //   件数が食い違い、`USE_FIXTURES` の true/false で結果が変わる（禁止事項6）
    expect(
      filterFixtureCleanLogs(cleanLogs, {
        id: null,
        date_from: null,
        date_to: null,
        facility_ids: [],
      })
    ).toEqual([]);
  });

  it("★#6c★ `facility_ids: null` → ★全件（#1 と同じ）★", () => {
    expect(
      filterFixtureCleanLogs(cleanLogs, {
        id: null,
        date_from: null,
        date_to: null,
        facility_ids: null,
      })
    ).toHaveLength(cleanLogs.length);
  });

  it("存在しない施設IDだけを渡すと0件（★`[]` との区別がついている★）", () => {
    expect(
      filterFixtureCleanLogs(cleanLogs, {
        id: null,
        date_from: null,
        date_to: null,
        facility_ids: [999_999],
      })
    ).toEqual([]);
  });

  it("日付と施設の AND が効く", () => {
    const id = cleanLogs[0].facility_id;
    const rows = filterFixtureCleanLogs(cleanLogs, {
      id: null,
      date_from: middleDay,
      date_to: middleDay,
      facility_ids: [id],
    });
    expect(
      rows.every(
        (r) => r.facility_id === id && r.scheduled_at.slice(0, 10) === middleDay
      )
    ).toBe(true);
  });

  it("★★段階3: `id` を指定すると1件だけ★★（★Prisma 版の `where.id` と同じ意味★）", () => {
    const target = cleanLogs[0];
    const rows = filterFixtureCleanLogs(cleanLogs, { ...ALL, id: target.id });
    expect(rows).toEqual([target]);
  });

  it("★★段階3: 存在しない `id` → 0件★★（★`null` に化けて全件になっていないことの検出★）", () => {
    expect(filterFixtureCleanLogs(cleanLogs, { ...ALL, id: 999_999_999 })).toEqual([]);
  });

  it("★段階3: `id` は他の条件と AND で重なる★（★範囲外の `id` は引けない★）", () => {
    const target = cleanLogs[0];
    const otherDay = days.find((d) => d !== target.scheduled_at.slice(0, 10))!;
    expect(
      filterFixtureCleanLogs(cleanLogs, {
        ...ALL,
        id: target.id,
        date_from: otherDay,
        date_to: otherDay,
      })
    ).toEqual([]);
  });

  it("★入力の配列を壊さない（純粋関数）★", () => {
    const before = cleanLogs.length;
    filterFixtureCleanLogs(cleanLogs, { id: null, date_from: middleDay, date_to: middleDay, facility_ids: [] });
    expect(cleanLogs).toHaveLength(before);
  });
});

/**
 * ★fixtures の予定外タスクの行（指示書BG §2-1。`docs/fixtures.md`）★ — `fixtureSource.cleanLogs()` だけが混ぜる。
 * `cleanLogs`（予定のある行。清掃結果・エリア・シードの鍵）には入れていない。
 */
describe("fixtures 版 cleanLogs() — 予定外タスクの行（指示書BG）", () => {
  it("一覧に予定外の行が2件入り、予定のある行と同じ新しい順に並ぶ（予定のある行どうしの順序は変えない）", async () => {
    const { db } = await import("@/lib/db");
    const rows = await db.cleanLogs(ALL);
    const unscheduled = rows.filter((r) => r.unscheduled);
    expect(unscheduled.map((r) => r.row_key)).toEqual(
      unscheduledCleanLogs.map((r) => r.row_key)
    );
    expect(unscheduled.every((r) => r.id === null && r.departed && !r.result_pending)).toBe(true);
    expect(rows.length).toBe(cleanLogs.length + unscheduledCleanLogs.length);
    // 新しい順
    for (let i = 1; i < rows.length; i += 1) {
      expect(rows[i - 1].scheduled_at >= rows[i].scheduled_at).toBe(true);
    }
    // 予定のある行どうしの順序は `cleanLogs` のまま
    expect(rows.filter((r) => !r.unscheduled).map((r) => r.row_key)).toEqual(cleanLogs.map((r) => r.row_key));
    // row_key は一意
    expect(new Set(rows.map((r) => r.row_key)).size).toBe(rows.length);
  });

  it("U1-1 / U1-2 の行（◎ 95.9 と、clean_area 0 で × だが出発 ✓）", () => {
    expect(unscheduledCleanLogs.map((r) => [r.facility_id, r.completion_rate, r.grade, r.departed, r.task_status])).toEqual([
      [1, 95.9, "◎", true, 4],
      [11, 0, "×", true, 6],
    ]);
  });

  it("施設・期間で絞ると予定外の行も同じ規則で絞られ、単一ID の取得には当たらない", async () => {
    const { db } = await import("@/lib/db");
    const f11 = await db.cleanLogs({ ...ALL, facility_ids: [11] });
    expect(f11.some((r) => r.unscheduled)).toBe(true);
    expect((await db.cleanLogs({ ...ALL, facility_ids: [] })).length).toBe(0);
    const day = unscheduledCleanLogs[0].scheduled_at.slice(0, 10);
    const sameDay = await db.cleanLogs({ ...ALL, date_from: day, date_to: day });
    expect(sameDay.filter((r) => r.unscheduled).map((r) => r.row_key)).toEqual([unscheduledCleanLogs[0].row_key]);
    const one = await db.cleanLogs({ ...ALL, id: cleanLogs[0].id });
    expect(one.map((r) => r.row_key)).toEqual([cleanLogs[0].row_key]);
  });
});
