import { CLEAN_TIMETABLE_ID_UNSCHEDULED, RUN_COMPLETION_RATE } from "@/lib/config";
import { cleanLogs, history } from "./clean-logs";
import { facilities } from "./facilities";
import { cleanTaskReportStatus } from "./task-status-rule";

/**
 * `clean_task_reports` のサンプルデータ（指示書AQ 決定2 / 担当F）
 *
 * ★何のためのファイルか★: 指示書AQ の決定2 で ★清掃実績の取得元が
 * `clean_status_histories` → `clean_task_reports` に変わる★。
 * ローカルDBにも fixtures にも ★1行も無い★ ため、切替後は全画面が0件になる（落とし穴5）。
 * ★このファイルと `scripts/seed-local-db.ts` の投入が、他担当の動作確認の土台★。
 *
 * ★担当F の時点では `lib/fixtures/` の既存ファイルを変更せずに追加した★（`lib/fixtures/index.ts` への
 * `export *` 1行を除く）。その後、担当J2 が `clean-logs.ts` に `task_status` を足し、担当M が
 * ★`status` の規則を `./task-status-rule.ts` に切り出して両ファイルから import する形にした★（循環 import の解消）。
 *
 * ---------------------------------------------------------------------------
 * ★★実データの形（出所: ユーザー提供CSV `clean_task_reports.csv` 189行。2026-09-30 受領）★★
 *
 * | 実測 | 値 |
 * |---|---|
 * | `report_id` | ★19桁の数値文字列★（例 `1295486100215435264`）。`varchar(20)` |
 * | `clean_timetable_id` | ★CSV では189行すべて NULL★。★NULL は予定外のタスク★（予定のある清掃には id が入る。`CLAUDE.md` §4.7。旧: 「今後 TeamWorks が埋める前提」— 誤り）。★`0` も予定外のタスク★（TeamWorks 回答 2026-10-27。`cleanTaskReportZeroTimetableRows`） |
 * | `status` | ★3:2件 / 4:161件 / 5:4件 / 6:22件★（値の意味は第10回 N-2 で受領。★6 は 3 と同じ「清掃キャンセル」（ユーザー指示 2026-10-30）★） |
 * | `task_area` | ★清掃エリアごとにほぼ一定★（例: あるエリアは19行すべて `878.524992`） |
 * | `clean_area` | ★毎回変動し、68.3%（129/189行）で `task_area` を超える★（★fixtures では再現しない。約束3★） |
 * | `clean_ratio` | ★面積比とは一致しない★（例: 面積比130.0% のとき `clean_ratio` は 76.0）。★列は完了率に使っていない★（D-155） |
 * | `ended_at` | ★nullable★ |
 * | `clean_time` | 秒（0 の行もある） |
 *
 * ---------------------------------------------------------------------------
 * ★★設計上の約束（読まずに直さないこと）★★
 *
 * 1. ★未実施の清掃には行を作らない★ — 指示書AQ 1-1 の
 *    「未実施 ＝ 本清掃の `clean_timetables` 行に対応する `clean_task_reports` の行が無い」。
 *    ★`clean_task_reports` には `clean_error` 相当の列が無い★ので、行の有無だけが未実施の根拠。
 *    ★既存 `lib/fixtures/clean-logs.ts` は `clean_error` を `departed ? normal : notExecuted`
 *    で導出していて `null` の行を作れない構造だった（`:66`）★が、ここは ★そもそも
 *    `clean_error` 列を持たない★ ので同じ轍は踏まない。
 *
 * 2. ★`clean_ratio` 列は完了率に使っていない★（D-155）。★列に戻すときのために、清掃ログ（`cleanLogs`）の
 *    `completion_rate` と同じ値を入れてある★（戻すときに fixtures を作り直さずに済む。指示書AS D6）。
 *
 * 3. ★`clean_area` は清掃ログの完了率から作る★（2026-10-02。D-155 / 指示書AS 段階6-1）—
 *    完了率は ★面積から `runCompletionRate()`（`min(clean_area ÷ task_area × 100, 100)` を小数第1位まで
 *    切り捨て）★ で決まるようになったので、面積を完了率と独立に作ると
 *    ★DB 経由（`USE_FIXTURES=false`）で `docs/fixtures.md` §2.1 の境界値（90.1 / 90.0 / 89.5 / 70.1 /
 *    70.0 / 10.1 / 10.0 / 9.9 / 0）が全部壊れる★。`cleanTaskReportCleanArea()` の docstring を参照。
 *    ★旧約束3（面積を `clean_ratio` から逆算しない。実測の分布 68.3% が `task_area` 超えに寄せる）は廃止★ —
 *    D-151 の「完了率 = `clean_ratio` 列」が本当に効いているかを確かめるための約束だったが、
 *    D-155 で完了率が面積から決まるようになり、前提が逆になった。
 *
 * 4. ★`status` は生値（数字）で持つ★ — 表示名にするのは画面・CSV（`formatCleanTaskStatus()`。D-155 項2）。
 *    ここで `CLEAN_ERROR` に対応づけたり日本語に直したりしない。
 *
 * 5. ★冪等★ — `Math.random()` を使わず、すべて ★清掃ログの id から決まる純粋関数★ にする。
 *    ★`scripts/seed-local-db.ts` は同じ関数を import する★ので、
 *    ★`USE_FIXTURES` の true/false で同じ値が出る★（ズレたらどちらかのバグ）。
 *
 * 用意したケースの一覧は `docs/fixtures.md` §2.6、期待値の固定は
 * `lib/fixtures/__tests__/clean-task-reports.test.ts`。
 */

/**
 * `clean_task_reports` の1行（`prisma/schema.prisma:517-558` の実DDL準拠）。
 *
 * ★`lib/types/index.ts` には置かない★ — あれは ★API のスキーマ名に揃える場所★
 * （`CLAUDE.md` §9.2）であり、これは ★DBテーブルの形★。加えて
 * `lib/types/index.ts` は指示書AQ の担当B の所有ファイルなので触らない。
 */
export interface CleanTaskReportFixture {
  /** ★19桁の数値文字列★（`varchar(20)`）。`cleanTaskReportId()` が作る */
  report_id: string;
  facility_id: number;
  section_id: number;
  /** ★シードは `sections.id` とロボットの id を同値に採番している★（1清掃エリア=1ロボット） */
  robot_id: number;
  /**
   * ★nullable★。★NULL は `IN` に一致せず「未実施」として集計される（落とし穴1）★。
   * ★`0`（`CLEAN_TIMETABLE_ID_UNSCHEDULED`）も予定外のタスク★（TeamWorks 回答 2026-10-27。`cleanTaskReportZeroTimetableRows`）
   */
  clean_timetable_id: number | null;
  serial_number: string;
  /** ★生値（3 / 4 / 5 / 6）。数字のまま持つ★（表示名は画面・CSV が付ける。D-155 項2。6 は 3 と同じ「清掃キャンセル」。ユーザー指示 2026-10-30） */
  status: number;
  /** 清掃対象面積 m²（`double(12,6)`）。★清掃エリアごとに一定★ */
  task_area: number;
  /**
   * 清掃面積 m²（`double(12,6)`）。★清掃ログの完了率から作る★（`runCompletionRate()` で元の完了率に戻る）。
   * ★完了率100の行だけ `task_area` を超える★（上限100の確認）。
   */
  clean_area: number;
  /** 清掃完了率 %（`double(4,1)`）。★完了率には使っていない★（D-155）。清掃ログの完了率と同じ値 */
  clean_ratio: number;
  /** ISO 8601（JST, +09:00） */
  started_at: string;
  /** ★nullable★（`prisma/schema.prisma:541`）。順序キーは `ended_at ?? started_at`（1-4） */
  ended_at: string | null;
  /** 清掃時間（秒）。★0 の行もある★ */
  clean_time: number;
}

// ---------------------------------------------------------------------------
// report_id
// ---------------------------------------------------------------------------

/**
 * `report_id` の先頭。★CSV実測の `1295486100215435264` と同じ19桁の帯★に収まるよう選んだ。
 *
 * ★`BigInt` リテラル（`123n`）は使わない★ — `tsconfig.json` の `target` が `ES2017` のため。
 */
const CLEAN_TASK_REPORT_ID_BASE = BigInt("1295486100000000000");

/** `report_id` の桁数（`varchar(20)` に収まることをテストで固定する） */
export const CLEAN_TASK_REPORT_ID_DIGITS = 19;

/**
 * 清掃（`clean_timetables.id`）から `report_id` を決定的に作る。
 *
 * ★`clean_timetable_id` を NULL にする行でも、report_id は元の清掃 id から作る★ —
 * 「どの清掃の実績か」は当方が分かっていて、★NULL にしているのは join キーだけ★
 * （落とし穴1 の再現）なので、id が衝突しない採番を保てる。
 */
export function cleanTaskReportId(cleanTimetableId: number): string {
  return String(CLEAN_TASK_REPORT_ID_BASE + BigInt(cleanTimetableId));
}

// ---------------------------------------------------------------------------
// status（3 / 4 / 5 / 6）
// ---------------------------------------------------------------------------

/*
 * ★status の規則（分布と巡回列）は `./task-status-rule.ts` が唯一の定義★。
 * ここでは再エクスポートするだけ（`scripts/seed-local-db.ts` と
 * `lib/fixtures/__tests__/clean-task-reports.test.ts` がこのファイルから import しているため）。
 *
 * ★ここに規則を書き戻さないこと★ — `lib/fixtures/clean-logs.ts` の `task_status` も同じ規則を使う。
 * `clean-logs.ts` はこのファイルを import できない（このファイルが読み込み時に `cleanLogs` / `history`
 * を読むため循環 import になる）ので、依存の無い葉モジュールに切り出してある。
 */
export {
  CLEAN_TASK_REPORT_STATUS_CYCLE_LENGTH,
  CLEAN_TASK_REPORT_STATUS_DISTRIBUTION,
  cleanTaskReportStatus,
} from "./task-status-rule";

// ---------------------------------------------------------------------------
// task_area / clean_area
// ---------------------------------------------------------------------------

/** `double(12,6)` に収める（小数第6位まで） */
function round6(value: number): number {
  return Math.round(value * 1_000_000) / 1_000_000;
}

/**
 * ★決定的な疑似乱数★（`lib/fixtures/clean-logs.ts` の `pseudoRandom()` と同じ式）。
 * ★`Math.random()` は使わない★ — 実行のたびに面積が変わると冪等性を確認できない。
 */
function pseudoRandom(seed: number): number {
  const x = Math.sin(seed * 12.9898) * 43758.5453;
  return x - Math.floor(x);
}

const TASK_AREA_MIN_SQM = 600;
const TASK_AREA_STEP_SQM = 25;
const TASK_AREA_STEPS = 17;

/**
 * 清掃エリア（= ロボット）ごとの `task_area`。★同じエリアなら常に同じ値★
 * （実測「あるエリアは19行すべて `878.524992`」に合わせた）。
 *
 * ★既存シードの `TASK_AREA_SQM = 1000`（全エリア一律）とは別物★ —
 * 一律だと「エリアごとに面積が違う」ことに起因する不具合（総清掃面積の按分など）が
 * ローカルでは絶対に出ない。600〜1000 m² の範囲で散らし、小数第6位まで持たせる。
 */
export function cleanTaskReportTaskArea(sectionId: number): number {
  const base = TASK_AREA_MIN_SQM + (sectionId % TASK_AREA_STEPS) * TASK_AREA_STEP_SQM;
  const fraction = ((sectionId * 524_992) % 1_000_000) / 1_000_000;
  return round6(base + fraction);
}

/** 完了率100（上限）の行で `task_area` を超えさせる係数の幅（1.00〜1.40倍） */
const CLEAN_AREA_OVER_TASK_AREA_MAX_EXTRA = 0.4;

/**
 * `clean_area`。★清掃ログの完了率から作る★（設計上の約束3。D-155 / 指示書AS 段階6-1）。
 *
 * - 完了率が上限（100）未満 … `round6(task_area × 完了率 ÷ 100)`
 * - 完了率が上限（100）    … `round6(task_area × 係数)`、係数は `pseudoRandom()` で 1.00〜1.40
 *   （★`task_area` を超える行の再現。`runCompletionRate()` の上限100の確認になる★）
 *
 * ★この作り方で `runCompletionRate({ clean_area, task_area })` が元の完了率に一致する★ —
 *   `lib/fixtures/__tests__/clean-task-reports.test.ts` が全行で確かめている
 *   （2026-10-02 に node で、小数第6位までの `task_area` 200通り × 完了率 0.0〜100.0 の0.1刻みで不一致0件。指示書AS 6-1）。
 * ★帰結★: fixtures の清掃ログに完了率100の行は無い（最大 99.5）ので、★`clean_area > task_area` の行は0件★。
 *   ★CSV の分布（68.3%）の再現はやめた★。
 * ★旧: `CLEAN_AREA_BELOW_TASK_AREA_SHARE`（0.278）で約3割を下回り・約7割を上回りに振り分け、
 *   完了率と独立に作っていた★（旧約束3。D-155 で廃止）。
 */
export function cleanTaskReportCleanArea(
  taskArea: number,
  completionRate: number,
  cleanTimetableId: number
): number {
  if (completionRate < RUN_COMPLETION_RATE.cap) {
    return round6((taskArea * completionRate) / 100);
  }
  const factor =
    1.0 + pseudoRandom(cleanTimetableId * 7 + 3) * CLEAN_AREA_OVER_TASK_AREA_MAX_EXTRA;
  return round6(taskArea * factor);
}

// ---------------------------------------------------------------------------
// ended_at（nullable）/ clean_time
// ---------------------------------------------------------------------------

/** 何件に1件 `ended_at` を NULL にするか（★落とし穴3 の再現★） */
const ENDED_AT_NULL_EVERY = 29;

/**
 * ★`ended_at` を NULL にする行か★（落とし穴3 / 指示書AQ 1-4）。
 *
 * ★判定（◎○△×）には影響しない★ — 面積は入ったままなので、
 * 壊れるとしたら ★「最新1件」の順序キー★ と ★非null を型で要求している箇所★ だけ。
 * そのため境界値ケースの行に当たっても安全で、ブロックを選ばず一律の規則にしてある。
 */
export function cleanTaskReportEndedAtIsNull(cleanTimetableId: number): boolean {
  return cleanTimetableId % ENDED_AT_NULL_EVERY === 0;
}

/** 本清掃の所要時間（分）。`scripts/seed-local-db.ts` の `MAIN_CLEAN_DURATION_MINUTES` と同値 */
export const CLEAN_TASK_REPORT_DURATION_MINUTES = 90;

/** `clean_time` の上限（秒） */
const CLEAN_TIME_MAX_SECONDS = CLEAN_TASK_REPORT_DURATION_MINUTES * 60;

/**
 * `clean_time`（秒）。★`ended_at` が NULL の行は 0★ —
 * 実測に「0 の行」があり、終了時刻が無い行に所要時間だけ入っているのは不自然なため。
 */
export function cleanTaskReportCleanTime(
  cleanTimetableId: number,
  endedAtIsNull: boolean
): number {
  if (endedAtIsNull) return 0;
  const r = pseudoRandom(cleanTimetableId * 13 + 5);
  return Math.round(CLEAN_TIME_MAX_SECONDS * (0.3 + 0.7 * r));
}

// ---------------------------------------------------------------------------
// clean_timetable_id（nullable）★落とし穴1★
// ---------------------------------------------------------------------------

/** 何件に1件 `clean_timetable_id` を NULL にするか */
const NULL_TIMETABLE_ID_EVERY = 37;

/**
 * ★`clean_timetable_id` を NULL にする清掃ログの id★（落とし穴1 の検証用）。
 *
 * ★★対象を `history`（daysBack 6〜30）に限っているのは意図★★ —
 * NULL の行は ★行が消えるのではなく「未実施」として集計される（fail-silent）★ ので、
 * `boundaryCases` / `escalationCases` / `statusCases` に当てると
 * ★`docs/fixtures.md` §2.1 の境界値10ケースと §2.2 のエスカレーションが壊れ、
 * 他担当が「90.0 が ◎ になる」ことを画面で確かめられなくなる★。
 *
 * ★`scripts/seed-local-db.ts` はこの Set をそのまま import して使う★ので、
 * ★ローカルDBと fixtures で NULL になる行が完全に一致する★。
 *
 * ★★既知の構造上の制約（意図した非対称。直さないこと）★★ —
 * この Set の清掃は、★DB 経由（`USE_FIXTURES=false`）では「出発失敗・`task_status` null」★に、
 * ★fixtures 経由（`cleanLogs`）では「実施済み・`task_status` あり」★に見える。
 *   - DB 経由: `clean_timetable_id` が NULL の行は `IN` に一致しないので、対応するレポート行が
 *     「無い」扱いになり、指示書AQ 1-1 により未実施（＝出発失敗と同一扱い）になる（落とし穴1 そのもの）。
 *   - fixtures 経由: `cleanLogs` の `departed` / `completion_rate` / `task_status` は
 *     `clean-logs.ts` の入力から決まり、このファイルの結果を見ない。
 * ★fixtures 側を DB 経由に合わせられない理由★: この Set は `completion_rate !== null` の行から作っている。
 *   `cleanLogs` 側でその行を未実施（`completion_rate: null`）にすると、★Set に入る行が無くなり（空になり）、
 *   落とし穴1 を再現するケースそのものが消える★。
 * ★したがって、この数行について2経路の画面表示が違うのは不具合ではない★（`docs/fixtures.md` §2.6）。
 * `lib/fixtures/__tests__/areas.test.ts` の一致テストは `report_id`（元の清掃 id から作る）で突き合わせるため、
 * この数行でも `task_status` と `status` は一致する。
 */
export const cleanTaskReportNullTimetableLogIds: ReadonlySet<number> = new Set(
  history
    .filter((log) => log.completion_rate !== null && log.id % NULL_TIMETABLE_ID_EVERY === 0)
    .map((log) => log.id)
);

// ---------------------------------------------------------------------------
// clean_timetable_id = 0（予定外のタスク。TeamWorks 回答 2026-10-27）
// ---------------------------------------------------------------------------

/**
 * ★`clean_timetable_id = 0` の行の `report_id` に使う番号の下駄★。
 * 清掃ログ由来の `report_id`（`cleanTaskReportId(log.id)`。`log.id` は 1〜99,999。`scripts/seed-local-db.ts` の
 * 採番規則）と ★重ならない帯★にする。
 */
const ZERO_TIMETABLE_REPORT_ID_OFFSET = 9_000_000;

/** 予定外のタスクを、元にした清掃ログの開始から何分後に始めたことにするか（★同じ夜の追加の清掃★） */
const ZERO_TIMETABLE_START_AFTER_MINUTES = 180;

/** 予定外のタスクの完了率（★◎○△× の境界に当たらない値★。完了率・実施回数には入らないので判定は出ない） */
const ZERO_TIMETABLE_COMPLETION_RATE = 55.5;

/**
 * ★`clean_timetable_id` が `0` の行★（予定外のタスク。TeamWorks 回答 2026-10-27）。
 *
 * ★清掃ログ（`cleanLogs`）に対応しない、追加の1行★ — NULL の行（`cleanTaskReportNullTimetableLogIds`）は
 *   既存の清掃ログの join キーを NULL にして作ったが、★こちらは既存の行を変えずに足す★
 *   （境界値・エスカレーション・他のテストの期待値を動かさないため）。
 *
 * ★期待する扱い★（`lib/db/timetable-link.ts` の `scheduledTimetableId()`）:
 *   - ★総清掃面積（F-08）の面積の合計には入る★（予定外も実際に清掃した広さ。D-155）
 *   - ★完了率・実施回数には入らない★／★重複の番人（`countDuplicatedTimetables()`）は数えない★
 *   - ★`IN` で予定から引くクエリには返って来ない★（予定の id に 0 は無い）
 *
 * ★元にする清掃ログ★: `history` のうち実施済み・NULL 対象でない最初の行（★施設・清掃エリア・ロボットだけ借りる★）。
 *   ★`scripts/seed-local-db.ts` はこの配列をそのまま投入する★（`USE_FIXTURES` の true/false で同じ行）。
 *   ★fixtures 版の画面（`USE_FIXTURES=true`）はこのファイルを読まない★（総清掃面積は `areaCompletions` から作る）。
 *
 * ★配列そのもの（`cleanTaskReportZeroTimetableRows`）はファイル末尾で作る★ — `robotBySerial` より前で呼ぶと
 *   初期化前の参照で落ちるため。
 */
function buildZeroTimetableRows(): CleanTaskReportFixture[] {
  const base = history.find(
    (log) => log.completion_rate !== null && !cleanTaskReportNullTimetableLogIds.has(log.id)
  );
  if (!base) {
    throw new Error("[fixtures/clean-task-reports] clean_timetable_id=0 の行の元にする清掃ログが history にありません");
  }
  const robot = robotBySerial.get(base.serial_number);
  if (!robot) {
    throw new Error(
      `[fixtures/clean-task-reports] serial=${base.serial_number}（clean_log id=${base.id}）に対応するロボットが lib/fixtures/facilities.ts にありません`
    );
  }
  const seed = ZERO_TIMETABLE_REPORT_ID_OFFSET + base.id;
  const taskArea = cleanTaskReportTaskArea(robot.robotId);
  const startedAt = addMinutesJstIso(base.scheduled_at, ZERO_TIMETABLE_START_AFTER_MINUTES);
  return [
    {
      report_id: cleanTaskReportId(seed),
      facility_id: base.facility_id,
      section_id: robot.robotId,
      robot_id: robot.robotId,
      // ★`0` を直書きしない★（`CLEAN_TIMETABLE_ID_UNSCHEDULED`）
      clean_timetable_id: CLEAN_TIMETABLE_ID_UNSCHEDULED,
      serial_number: base.serial_number,
      // ★清掃正常終了★（status の巡回列に入れない。`CLEAN_TASK_REPORT_STATUS_DISTRIBUTION` の数を変えないため）
      status: 4,
      task_area: taskArea,
      clean_area: cleanTaskReportCleanArea(taskArea, ZERO_TIMETABLE_COMPLETION_RATE, seed),
      clean_ratio: ZERO_TIMETABLE_COMPLETION_RATE,
      // ★予定外のタスクの `started_at` は実際に清掃を始めた時刻★（`CLAUDE.md` §4.7）
      started_at: startedAt,
      ended_at: addMinutesJstIso(startedAt, CLEAN_TASK_REPORT_DURATION_MINUTES),
      clean_time: cleanTaskReportCleanTime(seed, false),
    },
  ];
}

// ---------------------------------------------------------------------------
// 組み立て
// ---------------------------------------------------------------------------

/**
 * シリアル → `{ facility_id, robot_id }`。
 * ★シードと同じ規則★（`section_id` はロボットの id と同値。1清掃エリア=1ロボット）。
 */
const robotBySerial = new Map<string, { facilityId: number; robotId: number }>();
for (const facility of facilities) {
  for (const robot of facility.robots) {
    robotBySerial.set(robot.serial_number, {
      facilityId: facility.id,
      robotId: robot.id,
    });
  }
}

/**
 * ISO 8601（JST）の時刻に分を足す。★実行環境の TZ に依存させない★
 * （`getUTC*` で組み立てる。`scripts/seed-local-db.ts` の `toDbJst()` と同じ考え方）。
 */
function addMinutesJstIso(iso: string, minutes: number): string {
  const shiftedJstMs = new Date(iso).getTime() + minutes * 60_000 + 9 * 60 * 60 * 1000;
  const d = new Date(shiftedJstMs);
  const p = (n: number): string => String(n).padStart(2, "0");
  return (
    `${d.getUTCFullYear()}-${p(d.getUTCMonth() + 1)}-${p(d.getUTCDate())}` +
    `T${p(d.getUTCHours())}:${p(d.getUTCMinutes())}:${p(d.getUTCSeconds())}+09:00`
  );
}

function build(): CleanTaskReportFixture[] {
  const out: CleanTaskReportFixture[] = [];
  for (const log of cleanLogs) {
    // ★未実施＝行を作らない（指示書AQ 1-1）★。`clean_error` 相当の列が無いので行の有無が唯一の根拠
    if (log.completion_rate === null) continue;

    const robot = robotBySerial.get(log.serial_number);
    if (!robot) {
      // ★fail-loud★: 実施済みなのにロボットが引けないのは fixtures 側の不整合。
      //   （未実施の行だけは施設12＝ロボット0台があるので、上の `continue` で先に抜けている）
      throw new Error(
        `[fixtures/clean-task-reports] serial=${log.serial_number}（clean_log id=${log.id}）に対応するロボットが lib/fixtures/facilities.ts にありません`
      );
    }

    const endedAtIsNull = cleanTaskReportEndedAtIsNull(log.id);
    const taskArea = cleanTaskReportTaskArea(robot.robotId);

    out.push({
      report_id: cleanTaskReportId(log.id),
      facility_id: log.facility_id,
      section_id: robot.robotId,
      robot_id: robot.robotId,
      clean_timetable_id: cleanTaskReportNullTimetableLogIds.has(log.id) ? null : log.id,
      serial_number: log.serial_number,
      status: cleanTaskReportStatus(log.id),
      task_area: taskArea,
      // ★清掃ログの完了率から作る★（設計上の約束3。`runCompletionRate()` で元の完了率に戻る）
      clean_area: cleanTaskReportCleanArea(taskArea, log.completion_rate, log.id),
      // ★`clean_ratio` 列は完了率に使っていない。列に戻すときのために、清掃ログの完了率と同じ値を入れてある★
      //   （設計上の約束2。指示書AS D6）
      clean_ratio: log.completion_rate,
      started_at: log.scheduled_at,
      ended_at: endedAtIsNull
        ? null
        : addMinutesJstIso(log.scheduled_at, CLEAN_TASK_REPORT_DURATION_MINUTES),
      clean_time: cleanTaskReportCleanTime(log.id, endedAtIsNull),
    });
  }
  return out;
}

/**
 * `clean_task_reports` のサンプルデータ。
 * ★`cleanLogs` と同じ並び（`started_at` 降順）★。
 * ★清掃ログ由来の行だけ★ — `clean_timetable_id = 0` の追加の行（`cleanTaskReportZeroTimetableRows`）は含めない
 *   （この配列は「実施済みの清掃1回につき1行」をテストで固定しているため）。
 */
export const cleanTaskReports: CleanTaskReportFixture[] = build();

/**
 * ★`clean_timetable_id` が `0` の行★（予定外のタスク。TeamWorks 回答 2026-10-27）。
 * 作り方・期待する扱いは `buildZeroTimetableRows()` の docstring。
 */
export const cleanTaskReportZeroTimetableRows: readonly CleanTaskReportFixture[] = buildZeroTimetableRows();

