/**
* fixtures 版 `cleanArea()` の集計材料づくり(指示書AN ★段階3★ / §6-4)。
*
* ★★`USE_FIXTURES` の true/false で数字が変わってはいけない★★(指示書AC 禁止事項6)—
* Prisma 版が SQL に降ろした条件(`started_at` の範囲・施設ID)と ★同じ意味★ を再現し、
* ★★その先のロールアップは同じ `aggregateCleanArea()` を通します★★
* (`CLAUDE.md` §9.5。★fixtures 用に別の集計を書かないこと★)。
*
* ------------------------------------------------------------------------
* ★★なぜ `areaCompletions`(`lib/fixtures/areas.ts`)を材料にするのか★★
* ------------------------------------------------------------------------
* ★fixtures で面積(`clean_area` / `task_area`)を持っているのはこの1本だけ★です。
* `AreaCompletionRow` は ★1行 = 1清掃エリア × 1日★(= P1 では 1ロボット。`CLAUDE.md` §8)で、
* ★実DBの `clean_task_reports` の1行に対応します★(旧: `clean_status_histories`。指示書AQ 決定3)。
*
* ------------------------------------------------------------------------
* ★★`completion_rate === null` の行を数えないこと★★(★静かにズレる箇所★)
* ------------------------------------------------------------------------
* `lib/fixtures/areas.ts` の組み立て(`build()`)は
* ★「`completion_rate` が `null` の行は `task_area` を 0 にする」ことを検査で強制★しており、
* これは ★★「その清掃にはレポートの行が無い(出発失敗・未実施)」★★ という意味です(指示書AQ §1-1)。
*
* ★実DBではレポートの行そのものが存在しない★ので、実施回数にも入りません。
* ★ここで数えると `executed_count`(清掃実施回数)が fixtures だけ多くなります★
* (★面積は 0 なので合計は変わらず、件数だけが静かに食い違う★)。
*
* ------------------------------------------------------------------------
* ★★完了率は `runCompletionRate()` を通す★★(2026-10-02。D-155 / 指示書AS 段階4-3)
* ------------------------------------------------------------------------
* Prisma 版と同じく ★1回ごとの完了率を面積から `runCompletionRate()` で作り★、★常に★ Σ と件数に足す
* (`task_area = 0` も 0% として足す。P-5=B。D-158)。★fixtures の行の `completion_rate` をそのまま足さないこと★(規則が2箇所に散る)。
*
* ★★fixtures(`lib/fixtures/areas.ts`)には予定外(`clean_timetable_id` が NULL)の行が無い★★ —
* どの行も本清掃の清掃ログ(`clean_log_id`)に紐づいている。★予定外を完了率・実施回数から外す規則は
* Prisma 版の純粋関数 `cleanAreaStatsFromRows()` のテスト(表4)で確かめる★。
*/
import { runCompletionRate } from "@/lib/judge";
import type {
AreaCompletionRow,
CleanAreaDbQuery,
CleanAreaFacilityStats,
} from "@/lib/types";
/**
* fixtures の面積行から、施設ごとの集計材料を作る(★純粋関数★。DBアクセスを混ぜない)。
*
* ★日付の比較は文字列のまま行う★ — `AreaCompletionRow.date` は既に JST の `YYYY-MM-DD`
* なので、実行環境のタイムゾーンに依存しません(`filterFixtureAreaCompletions()` と同じ作法)。
* ★`new Date()` を挟むと、Prisma 版が壁時計値で絞っているのと基準がずれます★。
* ★`date_to` はその日を**含む**★(`dbDateRange()` と同じ契約)。
*
* ★★`facility_ids` は `!== null` で分岐すること★★(★罠1 / 落とし穴3★)—
* ★`null` = 絞らない / `[]` = 0件ヒット★。`?.length` で書くと
* ★「0件ヒット」が「全件」に化けます★(例外もテスト失敗も出ない)。
*
* ★戻りは `facility_id` の昇順★ — Prisma 側の `groupBy` は順序を約束しないが、
* ★fixtures が入力順に依存して揺れると、テストで固定できるものが減る★ので決め切っておく
* (★並びの正は `aggregateCleanArea()` の `compareRows()`★。ここは単なる決定性のため)。
*/
export function cleanAreaStatsFromFixtureAreas(
rows: readonly AreaCompletionRow[],
query: CleanAreaDbQuery
): CleanAreaFacilityStats[] {
const byFacility = new Map<number, CleanAreaFacilityStats>();
const facilityIds =
query.facility_ids !== null ? new Set(query.facility_ids) : null;
for (const row of rows) {
// ★「レポートの行が無い」行は実DBに存在しない★(上の docstring)
if (row.completion_rate === null) continue;
if (facilityIds !== null && !facilityIds.has(row.facility_id)) continue;
if (query.date_from !== null && row.date < query.date_from) continue;
if (query.date_to !== null && row.date > query.date_to) continue;
// ★1回ごとの完了率は `runCompletionRate()` だけが決める★(Prisma 版と同じ。指示書AS D1)
const rate = runCompletionRate(row);
const current = byFacility.get(row.facility_id);
if (current === undefined) {
byFacility.set(row.facility_id, {
facility_id: row.facility_id,
clean_area: row.clean_area,
task_area: row.task_area,
executed_count: 1,
// ★完了率の材料(1回ごとの完了率の単純平均。D-155)★ — ★`runCompletionRate()` を通す★
clean_ratio_sum: rate,
clean_ratio_count: 1,
});
continue;
}
// ★`_sum` の `null` は fixtures には無い★(面積は常に数値)ので `?? 0` は要らないが、
// ★型は Prisma 版と同じ `number | null`★なので足し込みでは `?? 0` を通す。
current.clean_area = (current.clean_area ?? 0) + row.clean_area;
current.task_area = (current.task_area ?? 0) + row.task_area;
current.executed_count += 1;
current.clean_ratio_sum = (current.clean_ratio_sum ?? 0) + rate;
current.clean_ratio_count += 1;
}
return [...byFacility.values()].sort((a, b) => a.facility_id - b.facility_id);
}