import test from "node:test";
import assert from "node:assert/strict";
import { once } from "node:events";
import { spawn } from "node:child_process";
import { rm } from "node:fs/promises";
import { createServer } from "node:http";

const DATA = "apps/api/data/applications.json";
const PROC = "apps/api/data/procedures.json";

async function startServer(extraEnv = {}) {
  await rm(DATA, { force: true }).catch(() => {});
  await rm(PROC, { force: true }).catch(() => {});
  const port = 20000 + Math.floor(Math.random() * 20000);
  const child = spawn(process.execPath, ["apps/api/server.mjs"], {
    env: { ...process.env, PORT: String(port), ...extraEnv },
    stdio: ["ignore", "pipe", "pipe"],
  });
  await Promise.race([
    once(child.stdout, "data"),
    once(child.stderr, "data").then(([c]) => { throw new Error(c.toString()); }),
  ]);
  return { child, base: `http://127.0.0.1:${port}` };
}

test("マイナンバーカード認証と証明書オンライン請求のフロー", async () => {
  const { child, base } = await startServer();
  const J = (r) => r.json();
  try {
    // health
    assert.equal((await fetch(`${base}/health`)).status, 200);

    // 未ログインで /me は 401
    assert.equal((await fetch(`${base}/api/me`)).status, 401);

    // PIN 誤り
    const badPin = await fetch(`${base}/api/auth/card-login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ cardSerial: "JPKI-0001", pin: "0000" }) });
    assert.equal(badPin.status, 401);

    // マイナンバーカードでログイン
    const login = await fetch(`${base}/api/auth/card-login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ cardSerial: "JPKI-0001", pin: "1234" }) });
    assert.equal(login.status, 200);
    const { token, citizen } = await login.json();
    assert.ok(token);
    assert.equal(citizen.familyNameKanji, "山田");
    const auth = { "content-type": "application/json", authorization: `Bearer ${token}` };

    // /me で本人情報
    const me = await fetch(`${base}/api/me`, { headers: auth }).then(J);
    assert.equal(me.residentId, "1000000001");

    // 証明書の種類
    const types = await fetch(`${base}/api/certificate-types`).then(J);
    assert.ok(types.some((t) => t.id === "juminhyo"));

    // 住民票の写しを2部・コンビニ交付で請求
    const create = await fetch(`${base}/api/certificate-requests`, { method: "POST", headers: auth, body: JSON.stringify({ typeId: "juminhyo", copies: 2, deliveryMethod: "CONVENIENCE", usage: "年金手続き" }) });
    assert.equal(create.status, 201);
    const app = await create.json();
    assert.equal(app.fee, 600); // 300*2
    assert.equal(app.status, "RECEIVED");
    assert.match(app.receiptNumber, /^R\d{8}-\d{4}$/);
    assert.equal(app.routedTo, "juki-web-reference");

    // 郵送は送料加算
    const postal = await fetch(`${base}/api/certificate-requests`, { method: "POST", headers: auth, body: JSON.stringify({ typeId: "kazei", copies: 1, deliveryMethod: "POSTAL" }) }).then(J);
    assert.equal(postal.fee, 440); // 300 + 140

    // 自分の申請一覧（2件）
    const list = await fetch(`${base}/api/certificate-requests`, { headers: auth }).then(J);
    assert.equal(list.length, 2);

    // 状態を進める（審査側模擬）→ 審査中
    const adv = await fetch(`${base}/api/certificate-requests/${app.id}/advance`, { method: "POST", headers: auth }).then(J);
    assert.equal(adv.status, "REVIEWING");
    assert.equal(adv.linkStatus, "ACCEPTED");

    // 取消
    const cancel = await fetch(`${base}/api/certificate-requests/${postal.id}/cancel`, { method: "POST", headers: auth }).then(J);
    assert.equal(cancel.status, "CANCELLED");

    // 別人の申請は見えない（別カードでログイン）
    const login2 = await fetch(`${base}/api/auth/card-login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ cardSerial: "JPKI-0002", pin: "5678" }) }).then(J);
    const other = await fetch(`${base}/api/certificate-requests/${app.id}`, { headers: { authorization: `Bearer ${login2.token}` } });
    assert.equal(other.status, 404);
  } finally {
    child.kill();
  }
});

test("転出届などの届出オンライン申請フロー（第2弾手続き）", async () => {
  const { child, base } = await startServer();
  const J = (r) => r.json();
  try {
    const login = await fetch(`${base}/api/auth/card-login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ cardSerial: "JPKI-0001", pin: "1234" }) }).then(J);
    const auth = { "content-type": "application/json", authorization: `Bearer ${login.token}` };

    // 手続きカタログは全業務を網羅（多数の手続き＋カテゴリ分類）
    const types = await fetch(`${base}/api/procedure-types`).then(J);
    assert.ok(types.length >= 70, `手続きは70種類以上（実際: ${types.length}）`);
    // IDの重複がない
    assert.equal(new Set(types.map((t) => t.id)).size, types.length, "手続きIDに重複がない");
    const moveout = types.find((t) => t.id === "moveout");
    assert.ok(moveout, "転出届が存在する");
    assert.ok(moveout.fields.some((f) => f.key === "moveOutDate"));
    assert.ok(moveout.categoryLabel, "カテゴリラベルが付与される");
    // 各業務の代表手続き＋仕様書突合で補完した手続きが揃っている
    for (const id of ["movein", "birth", "death", "childallowance", "kokuho-join", "nenkin-exempt", "kaigo-nintei", "shogai-service", "cremation", "elector-kijitsu",
      "child-amendment", "kyoiku-hoiku-nintei", "kokuho-shussan", "kokuho-sousai", "kaigo-kougaku", "shogai-techo", "jiritsu-iryo", "inkan-haishi", "tax-genmen", "fuzaisha-vote", "zaigai-elector",
      "koseki-tenseki", "koseki-youshi", "koseki-ninchi", "koseki-fujuri", "juki-fujuri", "kaigo-jutaku", "hosogu", "maisou", "tax-kozabunkae", "yubin-vote"]) {
      assert.ok(types.some((t) => t.id === id), `手続き ${id} が存在する`);
    }
    // select型フィールドが返る
    const worldchange = types.find((t) => t.id === "worldchange");
    assert.ok(worldchange.fields.some((f) => f.type === "select" && Array.isArray(f.options)));

    // 必須項目未入力は400
    const bad = await fetch(`${base}/api/procedure-applications`, { method: "POST", headers: auth, body: JSON.stringify({ typeId: "moveout", payload: { newAddress: "" } }) });
    assert.equal(bad.status, 400);

    // 転出届を申請
    const create = await fetch(`${base}/api/procedure-applications`, { method: "POST", headers: auth, body: JSON.stringify({ typeId: "moveout", payload: { moveOutDate: "2026-07-01", newAddress: "◯◯県△△市1-2-3", contactTel: "090-1234-5678" } }) });
    assert.equal(create.status, 201);
    const app = await create.json();
    assert.equal(app.status, "RECEIVED");
    assert.equal(app.routedTo, "juki-web-reference");
    assert.equal(app.payload.newAddress, "◯◯県△△市1-2-3");

    // 一覧に出る・進める・取消
    const list = await fetch(`${base}/api/procedure-applications`, { headers: auth }).then(J);
    assert.equal(list.length, 1);
    const adv = await fetch(`${base}/api/procedure-applications/${app.id}/advance`, { method: "POST", headers: auth }).then(J);
    assert.equal(adv.status, "REVIEWING");
    const cancel = await fetch(`${base}/api/procedure-applications/${app.id}/cancel`, { method: "POST", headers: auth }).then(J);
    assert.equal(cancel.status, "CANCELLED");

    // 未ログインでは申請不可
    const noauth = await fetch(`${base}/api/procedure-applications`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ typeId: "moveout", payload: {} }) });
    assert.equal(noauth.status, 401);
  } finally {
    child.kill();
  }
});

test("バックオフィス連携: 申請が実際に担当システムへ転送される", async () => {
  // スタブのバックオフィス（juki想定）を起動して inbound を受ける
  const received = [];
  const stub = createServer((req, res) => {
    let raw = "";
    req.on("data", (c) => (raw += c));
    req.on("end", () => {
      if (req.url === "/api/v1/link/application/inbound" && req.method === "POST") {
        received.push({ token: req.headers["x-link-token"], body: JSON.parse(raw || "{}") });
        res.writeHead(202, { "content-type": "application/json" });
        return res.end(JSON.stringify({ eventId: "EV-1", status: "ACCEPTED" }));
      }
      res.writeHead(404); res.end();
    });
  });
  await new Promise((r) => stub.listen(0, r));
  const stubPort = stub.address().port;
  const bases = JSON.stringify({ "juki-web-reference": `http://127.0.0.1:${stubPort}` });
  const { child, base } = await startServer({ BACKOFFICE_BASES: bases, PORTAL_LINK_TOKEN: "portal-link-dev-token" });
  const J = (r) => r.json();
  try {
    const login = await fetch(`${base}/api/auth/card-login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ cardSerial: "JPKI-0001", pin: "1234" }) }).then(J);
    const auth = { "content-type": "application/json", authorization: `Bearer ${login.token}` };
    // jukiに連携される転出届を申請
    const app = await fetch(`${base}/api/certificate-requests`, { method: "POST", headers: auth, body: JSON.stringify({ typeId: "juminhyo", copies: 1, deliveryMethod: "CONVENIENCE" }) }).then(J);
    assert.equal(app.linkStatus, "ACCEPTED", "連携先が応答すると ACCEPTED になる");
    assert.equal(app.remoteId, "EV-1");
    assert.equal(received.length, 1, "スタブが申請を1件受信");
    assert.equal(received[0].token, "portal-link-dev-token");
    assert.equal(received[0].body.applicant.name, "山田花子");
  } finally {
    child.kill();
    stub.close();
  }
});

test("バックオフィスからの処理状況コールバックが申請に反映される", async () => {
  const { child, base } = await startServer({ PORTAL_LINK_TOKEN: "portal-link-dev-token" });
  const J = (r) => r.json();
  try {
    const login = await fetch(`${base}/api/auth/card-login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ cardSerial: "JPKI-0001", pin: "1234" }) }).then(J);
    const auth = { "content-type": "application/json", authorization: `Bearer ${login.token}` };
    // 届出を申請（連携先未設定なので QUEUED）
    const app = await fetch(`${base}/api/procedure-applications`, { method: "POST", headers: auth, body: JSON.stringify({ typeId: "moveout", payload: { moveOutDate: "2026-08-01", newAddress: "東京都港区1-1", contactTel: "090-0000-0000" } }) }).then(J);
    assert.equal(app.status, "RECEIVED");

    const cb = (body, token = "portal-link-dev-token") => fetch(`${base}/api/link/callback`, { method: "POST", headers: { "content-type": "application/json", "x-link-token": token }, body: JSON.stringify(body) });

    // トークン不正は401
    assert.equal((await cb({ externalId: app.id, status: "PROCESSING" }, "wrong")).status, 401);
    // 不明なexternalIdは404
    assert.equal((await cb({ externalId: "nope", status: "PROCESSING" })).status, 404);
    // 不正なstatusは400
    assert.equal((await cb({ externalId: app.id, status: "???" })).status, 400);

    // PROCESSING → 審査中(REVIEWING)
    const r1 = await cb({ externalId: app.id, status: "PROCESSING" });
    assert.equal(r1.status, 200);
    const after1 = await fetch(`${base}/api/procedure-applications/${app.id}`, { headers: auth }).then(J);
    assert.equal(after1.status, "REVIEWING");
    assert.equal(after1.linkStatus, "ACCEPTED");

    // COMPLETED → 交付準備完了(READY)、履歴も増える
    await cb({ externalId: app.id, status: "COMPLETED" });
    const after2 = await fetch(`${base}/api/procedure-applications/${app.id}`, { headers: auth }).then(J);
    assert.equal(after2.status, "READY");
    assert.ok(after2.history.length >= 3);

    // 証明書請求にも反映される
    const cert = await fetch(`${base}/api/certificate-requests`, { method: "POST", headers: auth, body: JSON.stringify({ typeId: "juminhyo", copies: 1, deliveryMethod: "CONVENIENCE" }) }).then(J);
    await cb({ externalId: cert.id, status: "REJECTED" });
    const certAfter = await fetch(`${base}/api/certificate-requests/${cert.id}`, { headers: auth }).then(J);
    assert.equal(certAfter.status, "REJECTED");
  } finally {
    child.kill();
  }
});

test("PIN3回誤りでカードロック", async () => {
  const { child, base } = await startServer();
  try {
    for (let i = 0; i < 3; i++) {
      await fetch(`${base}/api/auth/card-login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ cardSerial: "JPKI-0002", pin: "9999" }) });
    }
    const locked = await fetch(`${base}/api/auth/card-login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ cardSerial: "JPKI-0002", pin: "5678" }) });
    assert.equal(locked.status, 423);
  } finally {
    child.kill();
  }
});
