import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
import path from "node:path";
import test from "node:test";
import { fileURLToPath } from "node:url";

import {
  candidateTargetsFromKubernetes,
  classifyDirectTargetBlockerScope,
  createLiveOperationPlan,
  hasRequiredM3Connection,
  kubernetesServiceIdentity,
  parseArgs,
  requireSafetyGates
} from "./dev-m3-hardware-loop-smoke.mjs";

const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const testOutputDir = "tmp/dev-m3-hardware-loop-smoke-test";

function runSmoke(args) {
  return execFileSync("node", ["scripts/dev-m3-hardware-loop-smoke.mjs", ...args], {
    cwd: repoRoot,
    encoding: "utf8",
    stdio: ["ignore", "pipe", "pipe"]
  });
}

test("default M3 smoke is source/read-only and never claims DEV-LIVE", async () => {
  await mkdir(path.join(repoRoot, testOutputDir), { recursive: true });
  const output = path.join(testOutputDir, "default-source-readonly.json");
  try {
    const stdout = runSmoke(["--output", output]);
    assert.match(stdout, /mode=dry-run/u);
    const report = JSON.parse(await readFile(path.join(repoRoot, output), "utf8"));

    assert.equal(report.dryRunPlan.mode, "plan-only");
    assert.equal(report.dryRunPlan.dryRunCallsLiveEndpoints, false);
    assert.equal(report.dryRunPlan.liveWriteWillRun, false);
    assert.equal(report.liveOperation.status, "not_run");
    assert.equal(report.liveOperation.operationId, "not_observed");
    assert.equal(report.summary.status, "blocked");
    assert.match(report.summary.result, /did not call DEV endpoints/u);
    assert.equal(
      report.liveChecks.find((check) => check.id === "direct-call-do-write-di-read")?.status,
      "not_run"
    );
  } finally {
    await rm(path.join(repoRoot, output), { force: true });
  }
});

test("--dry-run aliases the same source/read-only no-write path", async () => {
  await mkdir(path.join(repoRoot, testOutputDir), { recursive: true });
  const output = path.join(testOutputDir, "dry-run-source-readonly.json");
  try {
    runSmoke(["--dry-run", "--output", output]);
    const report = JSON.parse(await readFile(path.join(repoRoot, output), "utf8"));

    assert.equal(report.dryRunPlan.mode, "plan-only");
    assert.equal(report.dryRunPlan.liveWriteWillRun, false);
    assert.ok(
      report.dryRunPlan.failureClassifications.includes("patch_panel_wiring_missing")
    );
    assert.equal(report.liveOperation.status, "not_run");
  } finally {
    await rm(path.join(repoRoot, output), { force: true });
  }
});

test("M3 report validator rejects DRY-RUN evidence promoted to live pass", async () => {
  await mkdir(path.join(repoRoot, testOutputDir), { recursive: true });
  const output = path.join(testOutputDir, "dry-run-promoted-live.json");
  const promoted = path.join(testOutputDir, "dry-run-promoted-live-tampered.json");
  try {
    runSmoke(["--dry-run", "--output", output]);
    const report = JSON.parse(await readFile(path.join(repoRoot, output), "utf8"));
    report.liveOperation = {
      status: "pass",
      operationId: "op_fake_dry_run",
      traceId: "trc_fake_dry_run",
      auditId: "aud_fake_dry_run",
      evidenceId: "evd_fake_dry_run",
      summary: "Incorrectly promoted DRY-RUN output to DEV-LIVE."
    };
    await writeFile(path.join(repoRoot, promoted), JSON.stringify(report, null, 2));

    assert.throws(
      () =>
        execFileSync("node", ["scripts/validate-dev-gate-report.mjs", promoted], {
          cwd: repoRoot,
          encoding: "utf8",
          stdio: ["ignore", "pipe", "pipe"]
        }),
      (error) => {
        assert.match(
          `${error.message}\n${error.stderr ?? ""}`,
          /liveOperation\.status cannot be pass when only a DRY-RUN plan is present/u
        );
        return true;
      }
    );
  } finally {
    await rm(path.join(repoRoot, output), { force: true });
    await rm(path.join(repoRoot, promoted), { force: true });
  }
});

test("M3 safety gates keep incomplete live commands source/read-only", () => {
  assert.deepEqual(requireSafetyGates(parseArgs([])), {
    mode: "dry-run",
    liveWriteAllowed: false
  });
  assert.deepEqual(requireSafetyGates(parseArgs(["--live", "--confirm-dev"])), {
    mode: "dry-run",
    liveWriteAllowed: false
  });
  assert.deepEqual(requireSafetyGates(parseArgs(["--dry-run", "--live", "--confirm-dev", "--expect-non-prod"])), {
    mode: "dry-run",
    liveWriteAllowed: false
  });
  assert.deepEqual(requireSafetyGates(parseArgs(["--live", "--confirm-dev", "--expect-non-prod"])), {
    mode: "live",
    liveWriteAllowed: true
  });
  assert.deepEqual(requireSafetyGates(parseArgs(["--live", "--confirm-dev", "--confirmed-non-production"])), {
    mode: "live",
    liveWriteAllowed: true
  });
});

test("M3 blocker scopes classify missing identities and patch-panel route gaps", () => {
  assert.equal(classifyDirectTargetBlockerScope("m3-service-discovery"), "target_missing");
  assert.equal(classifyDirectTargetBlockerScope("m3-direct-target-missing"), "target_missing");
  assert.equal(classifyDirectTargetBlockerScope("m3-box-simu-identity"), "identity_not_distinct");
  assert.equal(classifyDirectTargetBlockerScope("m3-gateway-simu-identity"), "identity_not_distinct");
  assert.equal(classifyDirectTargetBlockerScope("m3-patch-panel-wiring"), "patch_panel_wiring_missing");

  const plan = createLiveOperationPlan();
  assert.equal(plan.evidenceLevel, "DRY-RUN");
  assert.equal(plan.liveWriteWillRun, false);
  assert.ok(plan.failureClassifications.includes("identity_not_distinct"));
  assert.ok(plan.failureClassifications.includes("patch_panel_wiring_missing"));
  assert.ok(plan.failureClassifications.includes("patch_panel_route_owner_invalid"));
  assert.match(plan.refusalPolicy.join("\n"), /source\/read-only/u);
});

test("patch-panel route preflight rejects missing res_boxsimu_1 DO1 to res_boxsimu_2 DI1 wiring or wrong owner", () => {
  const wrongRoute = hasRequiredM3Connection({
    status: {
      serviceId: "hwlab-patch-panel",
      metadata: {
        propagation: "patch-panel-only"
      },
      activeConnections: [
        {
          fromResourceId: "res_boxsimu_1",
          fromPort: "DO1",
          toResourceId: "res_boxsimu_1",
          toPort: "DI1"
        }
      ]
    },
    wiring: {
      constraints: {
        propagation: "patch-panel-only",
        localLoopbackSubstituteAllowed: false
      },
      connections: [
        {
          from: { resourceId: "res_boxsimu_1", port: "DO1" },
          to: { resourceId: "res_boxsimu_1", port: "DI1" },
          mode: "exclusive"
        }
      ]
    }
  });
  assert.equal(Boolean(wrongRoute.active), false);
  assert.equal(Boolean(wrongRoute.configured), false);
  assert.equal(Boolean(wrongRoute.owner), true);

  const requiredRoute = hasRequiredM3Connection({
    status: {
      serviceId: "hwlab-patch-panel",
      metadata: {
        propagation: "patch-panel-only"
      },
      activeConnections: [
        {
          fromResourceId: "res_boxsimu_1",
          fromPort: "DO1",
          toResourceId: "res_boxsimu_2",
          toPort: "DI1"
        }
      ]
    },
    wiring: {
      constraints: {
        propagation: "patch-panel-only",
        localLoopbackSubstituteAllowed: false
      },
      connections: [
        {
          from: { resourceId: "res_boxsimu_1", port: "DO1" },
          to: { resourceId: "res_boxsimu_2", port: "DI1" },
          mode: "exclusive"
        }
      ]
    }
  });
  assert.equal(Boolean(requiredRoute.active), true);
  assert.equal(Boolean(requiredRoute.configured), true);
  assert.equal(Boolean(requiredRoute.owner), true);

  const wrongOwner = hasRequiredM3Connection({
    status: {
      serviceId: "hwlab-box-simu",
      metadata: {
        propagation: "box-loopback"
      },
      activeConnections: requiredRoute.active ? [requiredRoute.active] : []
    },
    wiring: {
      constraints: {
        propagation: "patch-panel-only",
        localLoopbackSubstituteAllowed: false
      },
      connections: [
        {
          from: { resourceId: "res_boxsimu_1", port: "DO1" },
          to: { resourceId: "res_boxsimu_2", port: "DI1" },
          mode: "exclusive"
        }
      ]
    }
  });
  assert.equal(Boolean(wrongOwner.active), true);
  assert.equal(Boolean(wrongOwner.configured), true);
  assert.equal(Boolean(wrongOwner.owner), false);
});

test("Kubernetes discovery preserves current indexed M3 service identities", () => {
  const endpointSlice = (serviceName, address, port, targetRef, labels = {}) => ({
    metadata: {
      name: `${serviceName}-slice`,
      labels: {
        "kubernetes.io/service-name": serviceName,
        ...labels
      }
    },
    ports: [{ name: "http", port }],
    endpoints: [
      {
        addresses: [address],
        conditions: { ready: true },
        targetRef: { name: targetRef }
      }
    ]
  });

  const discovery = {
    status: "observed",
    source: "kubernetes-endpointslices",
    services: Object.fromEntries(
      [
        endpointSlice("hwlab-box-simu-1", "10.0.0.11", 7201, "hwlab-box-simu-0", {
          "hwlab.pikastech.local/instance-id": "res_boxsimu_1"
        }),
        endpointSlice("hwlab-box-simu-2", "10.0.0.12", 7201, "hwlab-box-simu-1", {
          "hwlab.pikastech.local/instance-id": "res_boxsimu_2"
        }),
        endpointSlice("hwlab-gateway-simu-1", "10.0.0.21", 7101, "hwlab-gateway-simu-0", {
          "hwlab.pikastech.local/instance-id": "gwsimu_1"
        }),
        endpointSlice("hwlab-gateway-simu-2", "10.0.0.22", 7101, "hwlab-gateway-simu-1", {
          "hwlab.pikastech.local/instance-id": "gwsimu_2"
        }),
        endpointSlice("hwlab-patch-panel", "10.0.0.31", 7301, "hwlab-patch-panel-abc")
      ].map((item) => {
        const identity = kubernetesServiceIdentity(item);
        const port = item.ports[0].port;
        const endpoints = item.endpoints.flatMap((endpoint) =>
          endpoint.addresses.map((address) => ({
            url: `http://${address}:${port}`,
            address,
            port,
            targetRef: endpoint.targetRef.name,
            serviceName: identity.serviceName,
            serviceId: identity.serviceId,
            targetId: identity.targetId,
            instanceId: identity.instanceId
          }))
        );
        return [
          identity.serviceName,
          {
            endpointSlice: item.metadata.name,
            ...identity,
            port,
            count: endpoints.length,
            endpoints
          }
        ];
      })
    )
  };

  const candidates = candidateTargetsFromKubernetes(discovery);

  assert.deepEqual(
    candidates.map((candidate) => [
      candidate.id,
      candidate.serviceName,
      candidate.serviceId,
      candidate.instanceId,
      candidate.baseUrl
    ]),
    [
      ["box-simu-1", "hwlab-box-simu-1", "hwlab-box-simu", "res_boxsimu_1", "http://10.0.0.11:7201"],
      ["box-simu-2", "hwlab-box-simu-2", "hwlab-box-simu", "res_boxsimu_2", "http://10.0.0.12:7201"],
      ["gateway-simu-1", "hwlab-gateway-simu-1", "hwlab-gateway-simu", "gwsimu_1", "http://10.0.0.21:7101"],
      ["gateway-simu-2", "hwlab-gateway-simu-2", "hwlab-gateway-simu", "gwsimu_2", "http://10.0.0.22:7101"],
      ["patch-panel", "hwlab-patch-panel", "hwlab-patch-panel", null, "http://10.0.0.31:7301"]
    ]
  );
});
