/**
 * ★★ガードテスト: `robot_error_reports` への UPDATE が4カラムのうち渡した列だけになること★★
 * （指示書AT §3-1 / 段階2）
 *
 * ------------------------------------------------------------------------
 * ★★何を守るためのテストか★★
 * ------------------------------------------------------------------------
 * `prisma/schema.prisma` の `RobotErrorReport.updated_at` に `@updatedAt` が付いていると、
 * ★`updateMany` でも Prisma が `updated_at = <アプリの時刻(UTC)>` を SET に足す★（実測）。
 *   ① 列単位 GRANT の更新用アカウントでは権限エラー（5列目の書き込み。`CLAUDE.md` §7 違反）
 *   ② JST の DB に UTC の壁時計値（9時間前）が入る
 * db pull のたびに `@updatedAt` を手で付け直す運用（D-059）があるので、
 * ★誰かが機械的に付け直したら、`npx prisma generate` → `npm test` でここが赤くなる★。
 *
 * ------------------------------------------------------------------------
 * ★★DBには接続しません★★
 * ------------------------------------------------------------------------
 * ★本物の生成クライアント★（`@/lib/generated/prisma/client`）に、
 * ★送られた SQL を記録するだけの偽ドライバアダプタ★をつなぐ。
 * 呼ぶのは ★本番と同じ書き込み関数★（`writeRobotErrorColumns()`）。テスト用に別の書き方をしない。
 */
import { describe, expect, it } from "vitest";
import type { SqlDriverAdapterFactory } from "@prisma/client/runtime/client";
import { PrismaClient } from "@/lib/generated/prisma/client";
import {
  robotErrorColumnsFromPatch,
  writeRobotErrorColumns,
} from "@/lib/db/robot-error-write";
import type { PrecheckStatusPatch } from "@/lib/types";

interface RecordedQuery {
  sql: string;
  args: unknown[];
}

/** SQL を記録するだけの偽アダプタ。★トランザクションを張られたら例外★（`updateMany` は1文のはず） */
function recordingAdapter(recorded: RecordedQuery[]): SqlDriverAdapterFactory {
  const info = { provider: "mysql" as const, adapterName: "dwatch-recording-fake" };
  return {
    ...info,
    async connect() {
      return {
        ...info,
        async queryRaw(params) {
          recorded.push({ sql: params.sql, args: params.args });
          return { columnTypes: [], columnNames: [], rows: [] };
        },
        async executeRaw(params) {
          recorded.push({ sql: params.sql, args: params.args });
          return 1;
        },
        async executeScript() {
          throw new Error("[fake adapter] executeScript は想定外です");
        },
        async startTransaction() {
          throw new Error("[fake adapter] トランザクションは想定外です（updateMany は1文の UPDATE のはず）");
        },
        getConnectionInfo() {
          return { supportsRelationJoins: false };
        },
        async dispose() {},
      };
    },
  };
}

async function runWrite(patch: PrecheckStatusPatch) {
  const recorded: RecordedQuery[] = [];
  const client = new PrismaClient({ adapter: recordingAdapter(recorded) });
  try {
    const result = await writeRobotErrorColumns(client, "RER-TEST-1", patch);
    return { recorded, result };
  } finally {
    await client.$disconnect();
  }
}

/** `UPDATE ... SET a = ?, b = ? WHERE ...` の SET 句の列名を取り出す */
function setColumns(sql: string): string[] {
  const match = /\bSET\s+([\s\S]+?)\s+WHERE\b/i.exec(sql);
  if (!match) throw new Error(`SET 句が見つかりません: ${sql}`);
  return match[1].split(",").map((part) => {
    const column = /`([^`]+)`\s*=/.exec(part.trim());
    if (!column) throw new Error(`列名を読めません: ${part}`);
    return column[1];
  });
}

function updateStatements(recorded: RecordedQuery[]): RecordedQuery[] {
  return recorded.filter((q) => /^\s*UPDATE\b/i.test(q.sql));
}

/** 引数の日時を ISO 文字列にそろえる（アダプタに来る形が文字列でも Date でも比較できるように） */
function asIso(value: unknown): unknown {
  return value instanceof Date ? value.toISOString() : value;
}

// `now` = 2026-10-02 08:15 JST（真の瞬間）
const NOW = new Date("2026-10-02T08:15:00+09:00");

describe("robot_error_reports への UPDATE（本物の生成クライアント＋偽アダプタ）", () => {
  it("★SET は渡したパッチの列と完全に一致し、updated_at を含まない★（3列）", async () => {
    const { recorded, result } = await runWrite({
      status: "in_progress",
      note: "現場に連絡済み",
      responded_at: NOW,
    });
    const updates = updateStatements(recorded);
    expect(updates).toHaveLength(1);
    expect(recorded).toHaveLength(1); // SELECT もトランザクションも無い1文
    const columns = setColumns(updates[0].sql);
    expect(columns).toEqual(["status", "comment", "responded_at"]);
    expect(columns).not.toContain("updated_at");
    expect(updates[0].sql).toMatch(/WHERE\s+.*`error_id`\s*=\s*\?/i);
    expect(result).toEqual({ matched: 1, columns: ["status", "comment", "responded_at"] });
  });

  it("★4列すべてを渡しても4列だけ★（updated_at を含まない）", async () => {
    const { recorded } = await runWrite({
      status: "resolved",
      note: null,
      responded_at: NOW,
      resolved_at: NOW,
    });
    const updates = updateStatements(recorded);
    expect(updates).toHaveLength(1);
    const columns = setColumns(updates[0].sql);
    expect([...columns].sort()).toEqual(["comment", "resolved_at", "responded_at", "status"]);
    expect(columns).not.toContain("updated_at");
  });

  it("1列だけ（status のみ）でも SET はその1列だけ", async () => {
    const { recorded } = await runWrite({ status: "in_progress" });
    const updates = updateStatements(recorded);
    expect(updates).toHaveLength(1);
    expect(setColumns(updates[0].sql)).toEqual(["status"]);
  });

  it("★日時の引数は壁時計値（toDbJst() を通した値）★ — 08:15 JST は \"2026-10-02T08:15:00.000Z\"", async () => {
    const { recorded } = await runWrite({ responded_at: NOW, resolved_at: null });
    const updates = updateStatements(recorded);
    expect(updates).toHaveLength(1);
    const columns = setColumns(updates[0].sql);
    const args = updates[0].args.map(asIso);
    expect(args[columns.indexOf("responded_at")]).toBe("2026-10-02T08:15:00.000Z");
    expect(args[columns.indexOf("resolved_at")]).toBeNull();
    // ★付け忘れると UTC の "2026-10-01T23:15:00.000Z" になる★
    expect(args).not.toContain("2026-10-01T23:15:00.000Z");
  });

  it("status は 0/1/2 に、where の引数は error_id の文字列のまま", async () => {
    const { recorded } = await runWrite({ status: "resolved" });
    const [update] = updateStatements(recorded);
    expect(update.args).toEqual([2, "RER-TEST-1"]);
  });
});

describe("robotErrorColumnsFromPatch（実行時の歯止め）", () => {
  it("★4つ以外のキーを渡すと例外★（updated_at / error_type）", () => {
    for (const key of ["updated_at", "error_type", "comment"]) {
      const patch = { status: "resolved", [key]: "x" } as unknown as PrecheckStatusPatch;
      expect(() => robotErrorColumnsFromPatch(patch)).toThrow(/書き込めないキー/);
    }
  });

  it("空パッチは例外（何も書かない要求はここまで来ない）", () => {
    expect(() => robotErrorColumnsFromPatch({})).toThrow(/書き込む列がありません/);
  });

  it("日時が Date でも null でもなければ例外", () => {
    const patch = { responded_at: "2026-10-02" } as unknown as PrecheckStatusPatch;
    expect(() => robotErrorColumnsFromPatch(patch)).toThrow(/Date か null/);
  });

  it("note → comment に写す（null は削除）", () => {
    expect(robotErrorColumnsFromPatch({ note: "a" })).toEqual({ comment: "a" });
    expect(robotErrorColumnsFromPatch({ note: null })).toEqual({ comment: null });
  });

  it("例外のメッセージに備考の中身を出さない", () => {
    const patch = { note: "秘密のメモ", extra: 1 } as unknown as PrecheckStatusPatch;
    expect(() => robotErrorColumnsFromPatch(patch)).toThrow();
    try {
      robotErrorColumnsFromPatch(patch);
    } catch (e) {
      expect(String(e)).not.toContain("秘密のメモ");
    }
  });
});
