import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import { randomUUID } from "node:crypto";
import { fileURLToPath } from "node:url";

import pg from "pg";

const { Pool } = pg;
const clients = positiveInteger(process.env.TASKTREE_MIGRATION_SMOKE_CLIENTS, 8);
const rounds = positiveInteger(process.env.TASKTREE_MIGRATION_SMOKE_ROUNDS, 3);
const adminUrl = process.env.TASKTREE_MIGRATION_SMOKE_DATABASE_URL
  || "postgresql:///postgres?host=/var/run/postgresql&user=root";
const databaseName = `tasktree_smoke_${randomUUID().replaceAll("-", "")}`;
const cliPath = fileURLToPath(new URL("../tools/hwlab-cli/bin/hwlab-cli.ts", import.meta.url));
const scopedUrl = new URL(adminUrl);
scopedUrl.pathname = `/${databaseName}`;
scopedUrl.searchParams.set("sslmode", "disable");
scopedUrl.searchParams.set("options", "-clock_timeout=1500ms");
const databaseUrl = scopedUrl.toString();
const admin = new Pool({ connectionString: adminUrl, max: 2 });
const scoped = new Pool({ connectionString: databaseUrl, max: 4 });

try {
  await admin.query(`CREATE DATABASE ${databaseName}`);

  const fresh = await Promise.all(Array.from({ length: clients }, () => runCli(["group", "list"])));
  fresh.forEach((result) => assertEnvelope(result, "group.list"));
  await assertMigrations();

  const createdGroup = await runCli(["group", "create", "--name", `Concurrency smoke ${Date.now()}`]);
  assertEnvelope(createdGroup, "group.create");
  const groupId = createdGroup.data.id;
  const createdTask = await runCli(["task", "create", "--group", groupId, "--title", "Read envelope seed"]);
  assertEnvelope(createdTask, "task.create");
  const taskId = createdTask.data.id;
  const report = await runCli(["report", "write", "--task", taskId, "--title", "Smoke", "--body", "passed"]);
  assertEnvelope(report, "report.write");
  assert.equal(report.data.mutation, true);

  await scoped.query(
    "DELETE FROM tasktree_schema_migrations WHERE migration_id = ANY($1::text[])",
    [["tasktree-20260717-v2-subsubtask", "tasktree-20260718-v3-github-backup"]],
  );
  await scoped.query("ALTER TABLE tasktree_tasks DROP CONSTRAINT tasktree_tasks_kind_check");
  await scoped.query(
    "INSERT INTO tasktree_tasks (id,group_id,kind,title) VALUES ($1,$2,$3,$4)",
    ["tt_invalid_migration_smoke", groupId, "invalid", "Migration rollback sentinel"],
  );
  const failedMigration = await runCli(["group", "list"], true);
  assert.equal(failedMigration.ok, false);
  assert.equal(failedMigration.error.code, "23514");
  assert.deepEqual(failedMigration.error.detail, {
    migrationId: "tasktree-20260717-v2-subsubtask",
    sqlState: "23514",
  });
  const afterFailure = await scoped.query("SELECT migration_id FROM tasktree_schema_migrations ORDER BY migration_id");
  assert.deepEqual(afterFailure.rows.map((row) => row.migration_id), ["tasktree-20260716-v1"]);

  await scoped.query("DELETE FROM tasktree_tasks WHERE id=$1", ["tt_invalid_migration_smoke"]);
  assertEnvelope(await runCli(["group", "list"]), "group.list");
  await assertMigrations();

  const blocker = await scoped.connect();
  try {
    await blocker.query("BEGIN");
    await blocker.query("SELECT id FROM tasktree_tasks LIMIT 1");
    const commands = [
      ["group", "list"],
      ["task", "list", "--group", groupId],
      ["report", "list", "--task", taskId],
    ];
    for (let round = 0; round < rounds; round += 1) {
      const results = await Promise.all(Array.from({ length: clients }, (_, index) => runCli(commands[index % commands.length])));
      results.forEach((result, index) => assertEnvelope(result, ["group.list", "task.list", "report.list"][index % commands.length]));
    }
    await blocker.query("COMMIT");
  } catch (error) {
    await blocker.query("ROLLBACK").catch(() => {});
    throw error;
  } finally {
    blocker.release();
  }

  console.log(JSON.stringify({
    ok: true,
    operation: "tasktree.schema-concurrency-smoke",
    data: {
      clients,
      rounds,
      freshCliEnvelopes: fresh.length,
      existingCliEnvelopes: clients * rounds,
      migrationRollbackVerified: true,
      existingReadsWithTableLockVerified: true,
      migrationIds: await migrationIds(),
    },
  }, null, 2));
} finally {
  await scoped.end().catch(() => {});
  await admin.query(`DROP DATABASE IF EXISTS ${databaseName} WITH (FORCE)`).catch(() => {});
  await admin.end().catch(() => {});
}

async function runCli(args, allowFailure = false) {
  const child = spawn(process.execPath, [cliPath, "tasktree", ...args], {
    cwd: fileURLToPath(new URL("..", import.meta.url)),
    env: { ...process.env, TASKTREE_DATABASE_URL: databaseUrl, DATABASE_URL: "" },
    stdio: ["ignore", "pipe", "pipe"],
  });
  let stdout = "";
  let stderr = "";
  child.stdout.setEncoding("utf8").on("data", (chunk) => { stdout += chunk; });
  child.stderr.setEncoding("utf8").on("data", (chunk) => { stderr += chunk; });
  const exitCode = await new Promise((resolve, reject) => {
    child.once("error", reject);
    child.once("close", resolve);
  });
  assert.equal(stderr.trim(), "", `CLI stderr must stay empty: ${stderr}`);
  assert.ok(stdout.trim(), "CLI must return one JSON envelope on stdout");
  const result = JSON.parse(stdout);
  if (!allowFailure) assert.equal(exitCode, 0, JSON.stringify(result));
  return result;
}

function assertEnvelope(result, operation) {
  assert.equal(result.ok, true, JSON.stringify(result));
  assert.equal(result.operation, operation);
  assert.ok(Object.hasOwn(result, "data"), "successful CLI envelope must contain data");
}

async function assertMigrations() {
  assert.deepEqual(await migrationIds(), [
    "tasktree-20260716-v1",
    "tasktree-20260717-v2-subsubtask",
    "tasktree-20260718-v3-github-backup",
  ]);
  const constraint = await scoped.query(
    "SELECT COUNT(*)::int AS count FROM pg_constraint WHERE conrelid='tasktree_tasks'::regclass AND conname='tasktree_tasks_kind_check'",
  );
  assert.equal(constraint.rows[0].count, 1);
}

async function migrationIds() {
  const result = await scoped.query("SELECT migration_id FROM tasktree_schema_migrations ORDER BY migration_id");
  return result.rows.map((row) => row.migration_id);
}

function positiveInteger(value, fallback) {
  const parsed = Number(value ?? fallback);
  assert.ok(Number.isInteger(parsed) && parsed > 0, "smoke concurrency parameters must be positive integers");
  return parsed;
}
