import { describe, expect, it } from "vitest";

// ★ルート本体は**静的 import** すること★（`app/api/reports/__tests__/route.test.ts` の実測）
import { GET } from "@/app/api/clean-area/route";
import { facilities } from "@/lib/fixtures";
import { includesFoldName } from "@/lib/format/search-text";
import type { CleanAreaResponse } from "@/lib/types";

/**
 * `GET /clean-area` を ★fixtures（`USE_FIXTURES` の既定 = true）★ で通すテスト。
 *
 * ------------------------------------------------------------------------
 * ★★なぜ `route.test.ts` と分けてあるのか★★
 * ------------------------------------------------------------------------
 * ルートは ★施設名の解決と分類の解決を `Promise.all` で並列に★ 走らせる
 * （`resolveFacilityIdsByName()` / `resolveFacilityIdsByMasters()`。指示書AK §6-2）。
 *
 * ★★`route.test.ts` の `vi.mock("@/lib/db/prisma")` ではこの経路を試せません★★ —
 *   `lib/db/index.ts` は `await import("./prisma")` で Prisma を動的に読むため、
 *   ★同じ瞬間に2本の動的 import が走ると、片方が差し替えをすり抜けて本物を掴み、
 *   「DATABASE_URL が無い」で 500 になります★（実測。テスト環境側の制約であって
 *   ルートの不具合ではない）。
 * ★fixtures 経路は動的 import を通らない★ので、★★積集合（AND）はこちらで固定します★★。
 *
 * ------------------------------------------------------------------------
 * ★★期待値は fixtures から組み立てる★★（`app/api/__tests__/master-filter.test.ts` と同じ作法）
 * ------------------------------------------------------------------------
 * 施設の割り当てが変わってもテストが「件数だけ違う」で落ちないようにするため。
 * ★代わりに「絞る前後で必ず差が出ること」を明示的に確かめる★
 * （＝絞り込みが本当に効いたことの確認）。
 */

async function body(qs = ""): Promise<CleanAreaResponse> {
  const res = await GET(new Request(`http://localhost/api/clean-area${qs}`));
  expect(res.status).toBe(200);
  return (await res.json()) as CleanAreaResponse;
}

/** 面積を持つ施設だけ（fixtures の面積行が無い施設は内訳に現れない） */
async function facilityIdsInResponse(qs = ""): Promise<number[]> {
  const res = await body(qs);
  return res.items.map((row) => row.id as number).sort((a, b) => a - b);
}

describe("GET /clean-area（fixtures）", () => {
  it("#F1 ★粒度を変えても全体合計が変わらない★（§9-3 の 9-8 / B-5）", async () => {
    const byFacility = await body("?group_by=facility");
    const byCustomer = await body("?group_by=customer");
    const byGroup = await body("?group_by=group");

    expect(byFacility.total.clean_area).toBeGreaterThan(0);
    expect(byCustomer.total).toEqual(byFacility.total);
    expect(byGroup.total).toEqual(byFacility.total);

    // ★内訳の和 ＝ 全体合計★（粒度ごとに確かめる。落とし穴4 / 落とし穴5）
    for (const res of [byFacility, byCustomer, byGroup]) {
      const sum = res.items.reduce((acc, row) => acc + row.clean_area, 0);
      expect(sum).toBeCloseTo(res.total.clean_area, 6);
    }
    // 粒度を上げるほど行数は減る（同数のままなら畳めていない）
    expect(byGroup.items.length).toBeLessThan(byFacility.items.length);
  });

  it("#F2 ★分類で絞ると内訳がその傘下だけになる★", async () => {
    const all = await facilityIdsInResponse();
    const groupId = facilities[0].group_id;
    expect(groupId).not.toBeNull();

    const filtered = await facilityIdsInResponse(`?group_id=${groupId}`);
    const allowed = new Set(
      facilities.filter((f) => f.group_id === groupId).map((f) => f.id)
    );
    expect(filtered.every((id) => allowed.has(id))).toBe(true);
    // ★絞り込みが本当に効いたことの確認★ — 絞る前には傘下でない施設が居た
    expect(all.some((id) => !allowed.has(id))).toBe(true);
    expect(filtered.length).toBeLessThan(all.length);
  });

  it("#F3 ★★施設名 × 分類は AND（積集合）★★（指示書AK §6-2 / 落とし穴3）", async () => {
    // ★`Promise.all` で並列解決される2つの条件★が両方効いていることを見る。
    //   ★キーワードは「2つ以上の屋号にまたがる」ものを選ぶこと★ —
    //   1屋号にしか当たらない語だと、AND を無視しても同じ結果になり ★テストが無意味になる★。
    const customerId = facilities[0].customer_id;
    const keyword = "バリューマート";
    const byName = facilities.filter((f) => includesFoldName(f.facility_name, keyword));
    // ★前提そのものを固定する★（fixtures が変わったらここで落ちる）
    expect(new Set(byName.map((f) => f.customer_id)).size).toBeGreaterThan(1);

    const expected = byName.filter((f) => f.customer_id === customerId).map((f) => f.id);
    expect(expected.length).toBeGreaterThan(0);

    const got = await facilityIdsInResponse(
      `?customer_id=${customerId}&facility_name=${encodeURIComponent(keyword)}`
    );
    // ★面積行を持たない施設は内訳に出ない★ので「期待の部分集合であること」を見る
    expect(got.every((id) => expected.includes(id))).toBe(true);

    // ★どちらか片方を捨てていないこと★ —
    //   施設名だけで引くと、別の屋号の施設まで入ってくる（＝ AND が効いていれば入らない）
    const byNameOnly = await facilityIdsInResponse(
      `?facility_name=${encodeURIComponent(keyword)}`
    );
    expect(byNameOnly.length).toBeGreaterThanOrEqual(got.length);
    expect(
      byNameOnly.every((id) => facilities.find((f) => f.id === id)?.customer_id === customerId)
    ).toBe(false);
  });

  it("#F4 ★実在しない group_id は0件（全件に化けない）★（§9-2 の 9-4）", async () => {
    const res = await body("?group_id=9999");
    expect(res.items).toEqual([]);
    expect(res.total).toEqual({
      clean_area: 0,
      task_area: 0,
      // ★分母0は `null`★（0% ではない）
      completion_rate: null,
      executed_count: 0,
    });
    expect(res.meta).toEqual({ total: 0, page: 1, per_page: 0 });
  });

  it("#F5 期間で絞ると合計が減る（§9-2 の 9-3）", async () => {
    const all = await body();
    // fixtures の1日だけを切り出す（★どの日でもよいので全期間より小さくなればよい★）
    const narrowed = await body("?date_from=2026-09-10&date_to=2026-09-10");
    expect(narrowed.total.clean_area).toBeLessThan(all.total.clean_area);
    expect(narrowed.total.executed_count).toBeLessThan(all.total.executed_count);
  });
});
