import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import path from "node:path";
import test from "node:test";
import { promisify } from "node:util";

import {
  buildDevRuntimeProvisioningReport,
  parseArgs
} from "./dev-runtime-provisioning.mjs";

const execFileAsync = promisify(execFile);
const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../..");
const bunCommand = process.env.HWLAB_BUN_COMMAND || (process.versions.bun ? process.execPath : "bun");

test("source check parses target role/database without leaking DB URL material", async () => {
  const report = await buildDevRuntimeProvisioningReport(parseArgs(["--check"]), {
    env: {
      HWLAB_CLOUD_DB_URL: fixturePostgresUrl({ password: fixtureSecret("super") }),
      HWLAB_CLOUD_DB_SSL_MODE: "disable"
    },
    now: () => "2026-05-23T00:00:00.000Z"
  });

  assert.equal(report.conclusion.status, "ready");
  assert.equal(report.actions.liveDbReadAttempted, false);
  assert.equal(report.actions.liveDbWriteAttempted, false);
  assert.equal(report.db.targetRoleNamePresent, true);
  assert.equal(report.db.targetDatabaseNamePresent, true);
  assert.equal(report.db.targetPasswordPresent, true);
  assert.equal(report.safety.secretValuesPrinted, false);
  assertNoFixtureSecrets(report);
});

test("cloud-api image provisioning entrypoint exposes non-secret source check", async () => {
  const { stdout } = await execFileAsync(
    bunCommand,
    ["cmd/hwlab-cloud-api/provision.ts", "--check"],
    {
      cwd: repoRoot,
      env: {
        ...process.env,
        HWLAB_CLOUD_DB_URL: fixturePostgresUrl({ password: fixtureSecret("super") }),
        HWLAB_CLOUD_DB_SSL_MODE: "disable"
      }
    }
  );
  const report = JSON.parse(stdout);

  assert.equal(report.conclusion.status, "ready");
  assert.equal(report.actions.liveDbReadAttempted, false);
  assert.equal(report.actions.liveDbWriteAttempted, false);
  assert.equal(report.db.targetRoleNamePresent, true);
  assert.equal(report.db.targetDatabaseNamePresent, true);
  assert.equal(report.db.targetPasswordPresent, true);
  assert.equal(report.safety.secretValuesPrinted, false);
  assertNoFixtureSecrets(report);
});

test("source check without injected DB URL is ready and performs no live access", async () => {
  const report = await buildDevRuntimeProvisioningReport(parseArgs(["--check"]), {
    env: {},
    now: () => "2026-05-23T00:00:00.000Z"
  });

  assert.equal(report.conclusion.status, "ready");
  assert.equal(report.actions.liveDbReadAttempted, false);
  assert.equal(report.actions.liveDbWriteAttempted, false);
  assert.equal(report.blockers.length, 0);
  assert.equal(report.db.targetUrlPresent, false);
  assert.equal(report.gates.role.status, "not_checked");
});

test("live dry-run classifies missing target role separately from missing database", async () => {
  const report = await buildDevRuntimeProvisioningReport(
    parseArgs(["--dry-run", "--allow-live-db-read", "--confirm-dev"]),
    {
      env: envWithAdmin(),
      adminClient: createAdminClient({ roleExists: false, databaseExists: true }),
      queryClient: createRuntimeClient("auth"),
      now: () => "2026-05-23T00:00:00.000Z"
    }
  );

  assert.equal(report.conclusion.status, "blocked");
  assert.equal(report.actions.adminInspectionAttempted, true);
  assert.equal(report.actions.liveDbWriteAttempted, false);
  assert.equal(report.provisioning.targetRole.exists, false);
  assert.equal(report.provisioning.targetDatabase.exists, true);
  assert.equal(report.blockers.some((blocker) => blocker.scope === "runtime-provisioning-role"), true);
  assert.equal(report.blockers.some((blocker) => blocker.scope === "runtime-provisioning-database"), false);
  assert.equal(report.blockers.some((blocker) => blocker.scope === "runtime-provisioning-auth"), true);
  assert.equal(
    report.blockers.find((blocker) => blocker.scope === "runtime-provisioning-role").evidence.affectedRuntimeBlocker,
    "runtime_durable_adapter_auth_blocked"
  );
  assert.equal(
    report.blockers.find((blocker) => blocker.scope === "runtime-provisioning-auth").evidence.blocker,
    "runtime_durable_adapter_auth_blocked"
  );
  assertNoFixtureSecrets(report);
});

test("live dry-run classifies missing target database separately from target role", async () => {
  const report = await buildDevRuntimeProvisioningReport(
    parseArgs(["--dry-run", "--allow-live-db-read", "--confirm-dev"]),
    {
      env: envWithAdmin(),
      adminClient: createAdminClient({ roleExists: true, databaseExists: false }),
      queryClient: createRuntimeClient("auth"),
      now: () => "2026-05-23T00:00:00.000Z"
    }
  );

  assert.equal(report.conclusion.status, "blocked");
  assert.equal(report.provisioning.targetRole.exists, true);
  assert.equal(report.provisioning.targetDatabase.exists, false);
  assert.equal(report.blockers.some((blocker) => blocker.scope === "runtime-provisioning-role"), false);
  assert.equal(report.blockers.some((blocker) => blocker.scope === "runtime-provisioning-database"), true);
  assert.equal(
    report.blockers.find((blocker) => blocker.scope === "runtime-provisioning-database").evidence.affectedRuntimeBlocker,
    "runtime_durable_adapter_auth_blocked"
  );
});

test("live dry-run separates SSL from auth/schema/migration", async () => {
  const report = await buildDevRuntimeProvisioningReport(
    parseArgs(["--dry-run", "--allow-live-db-read", "--confirm-dev"]),
    {
      env: envWithAdmin({ sslMode: "require" }),
      adminClient: createAdminClient({ roleExists: true, databaseExists: true }),
      queryClient: createRuntimeClient("ssl"),
      now: () => "2026-05-23T00:00:00.000Z"
    }
  );

  assert.equal(report.conclusion.status, "blocked");
  assert.equal(report.gates.ssl.status, "blocked");
  assert.equal(report.gates.auth.status, "not_checked");
  assert.equal(report.gates.schema.status, "not_checked");
  assert.equal(report.gates.migration.status, "not_checked");
  assert.equal(report.blockers.some((blocker) => blocker.scope === "runtime-provisioning-ssl"), true);
  assert.equal(JSON.stringify(report).includes("does not support SSL"), false);
});

test("live dry-run leaves role/database ready while migration remains a downstream blocker", async () => {
  const report = await buildDevRuntimeProvisioningReport(
    parseArgs(["--dry-run", "--allow-live-db-read", "--confirm-dev"]),
    {
      env: envWithAdmin(),
      adminClient: createAdminClient({ roleExists: true, databaseExists: true }),
      queryClient: createRuntimeClient("migration"),
      now: () => "2026-05-23T00:00:00.000Z"
    }
  );

  assert.equal(report.conclusion.status, "ready");
  assert.equal(report.provisioning.ready, true);
  assert.equal(report.provisioning.targetRole.ready, true);
  assert.equal(report.provisioning.targetDatabase.ready, true);
  assert.equal(report.runtime.blocker, "runtime_durable_adapter_migration_blocked");
  assert.equal(report.gates.migration.status, "blocked");
  assert.equal(report.blockers.length, 0);
});

test("apply creates missing role/database and grants target privileges without printing secrets", async () => {
  const adminClient = createAdminClient({ roleExists: false, databaseExists: false });
  const targetAdminClient = createAdminClient({ roleExists: true, databaseExists: true });
  const report = await buildDevRuntimeProvisioningReport(
    parseArgs(["--apply", "--confirm-dev", "--confirmed-non-production"]),
    {
      env: envWithAdmin(),
      adminClient,
      queryClient: createRuntimeClient("ready"),
      now: () => "2026-05-23T00:00:00.000Z",
      adminDbUrlOverride: null,
      pgModuleLoader: async () => {
        throw new Error("pgModuleLoader should not be used when adminClient is injected");
      }
    }
  );

  assert.equal(targetAdminClient.calls.length, 0);
  assert.equal(report.conclusion.status, "ready");
  assert.equal(report.actions.roleCreated, true);
  assert.equal(report.actions.databaseCreated, true);
  assert.equal(report.actions.rolePasswordSynchronized, true);
  assert.equal(report.actions.databaseConnectGranted, true);
  assert.equal(report.actions.schemaPrivilegesGranted, true);
  assert.equal(report.provisioning.ready, true);
  assert.ok(adminClient.calls.some((call) => call.sql.startsWith("CREATE ROLE ")));
  assert.ok(adminClient.calls.some((call) => call.sql.startsWith("CREATE DATABASE ")));
  assert.ok(adminClient.calls.some((call) => call.sql.startsWith("GRANT CONNECT ON DATABASE ")));
  assert.ok(adminClient.calls.some((call) => call.sql.startsWith("GRANT USAGE, CREATE ON SCHEMA public ")));
  assertNoFixtureSecrets(report);
});

function envWithAdmin({ sslMode = "disable" } = {}) {
  return {
    HWLAB_CLOUD_DB_URL: fixturePostgresUrl({ password: fixtureSecret("app") }),
    HWLAB_CLOUD_DB_ADMIN_URL: fixturePostgresUrl({
      user: "postgres",
      password: fixtureSecret("admin"),
      database: "postgres"
    }),
    HWLAB_CLOUD_DB_SSL_MODE: sslMode
  };
}

function fixturePostgresUrl({
  user = "hwlab_app",
  password = fixtureSecret("app"),
  host = "db.example.test",
  database = "hwlab_runtime"
} = {}) {
  return `${["postgresql", "://"].join("")}${user}:${password}@${host}:5432/${database}`;
}

function fixtureSecret(kind) {
  return `${kind}-${"secret"}`;
}

function assertNoFixtureSecrets(value) {
  const text = JSON.stringify(value);
  for (const forbidden of [
    fixtureSecret("super"),
    fixtureSecret("app"),
    fixtureSecret("admin"),
    "db.example.test",
    "hwlab_app",
    "hwlab_runtime"
  ]) {
    assert.equal(text.includes(forbidden), false);
  }
}

function createAdminClient({ roleExists, databaseExists }) {
  const state = {
    roleExists,
    databaseExists,
    calls: []
  };
  return {
    get calls() {
      return state.calls;
    },
    async query(sql, params = []) {
      state.calls.push({ sql, params });
      if (sql.includes("pg_catalog.pg_roles")) {
        return { rows: state.roleExists ? [{ present: 1 }] : [] };
      }
      if (sql.includes("pg_catalog.pg_database")) {
        return { rows: state.databaseExists ? [{ present: 1 }] : [] };
      }
      if (sql.startsWith("CREATE ROLE ")) {
        state.roleExists = true;
        return { rows: [] };
      }
      if (sql.startsWith("ALTER ROLE ")) {
        return { rows: [] };
      }
      if (sql.startsWith("CREATE DATABASE ")) {
        state.databaseExists = true;
        return { rows: [] };
      }
      if (sql.startsWith("GRANT CONNECT ON DATABASE ") || sql.startsWith("GRANT USAGE, CREATE ON SCHEMA public ")) {
        return { rows: [] };
      }
      throw new Error(`unexpected admin query: ${sql}`);
    }
  };
}

function createRuntimeClient(mode) {
  return {
    async query(sql) {
      if (sql.includes("information_schema.columns")) {
        if (mode === "ssl") {
          const error = new Error("fixture SSL blocked: server does not support SSL");
          error.code = "08P01";
          throw error;
        }
        if (mode === "auth") {
          const error = new Error("fixture auth blocked");
          error.code = "28P01";
          throw error;
        }
        return { rows: schemaRows() };
      }
      if (sql.startsWith("SELECT id, schema_version FROM hwlab_schema_migrations")) {
        if (mode === "migration") return { rows: [] };
        return { rows: [{ id: "0001_cloud_core_skeleton", schema_version: "runtime-durable-postgres-v1" }] };
      }
      if (sql.startsWith("SELECT COUNT(*)::int AS count FROM ")) {
        return { rows: [{ count: 0 }] };
      }
      throw new Error(`unexpected runtime query: ${sql}`);
    }
  };
}

function schemaRows() {
  const schema = {
    gateway_sessions: ["id", "project_id", "gateway_service_id", "status", "started_at", "ended_at", "gateway_session_json"],
    box_resources: ["id", "project_id", "gateway_session_id", "resource_state", "labels_json", "resource_json", "updated_at"],
    box_capabilities: ["id", "box_resource_id", "capability_type", "capability_json", "updated_at"],
    hardware_operations: ["id", "project_id", "requested_by", "operation_type", "operation_json", "status", "requested_at", "updated_at"],
    audit_events: ["id", "request_id", "actor", "source", "operation", "target", "result", "timestamp", "event_json"],
    evidence_records: ["id", "project_id", "operation_id", "evidence_type", "uri", "metadata_json", "created_at"]
  };
  return Object.entries(schema).flatMap(([table, columns]) =>
    columns.map((column) => ({ table_name: table, column_name: column }))
  );
}
