#!/usr/bin/env node
import assert from "node:assert/strict";
import os from "node:os";
import path from "node:path";
import { promises as fs } from "node:fs";
import { spawn, spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import { bootstrapTarget } from "../packages/sj-core/dist/core/bootstrap.js";
import { loadCanonicalConfig } from "../packages/sj-core/dist/core/config.js";
import { failWorkflowStep } from "../packages/sj-core/dist/core/workflow-engine.js";

const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const desktopExecutable = path.join(
  repoRoot,
  "apps",
  "sj-ui",
  "src-tauri",
  "target",
  "release",
  "bundle",
  "macos",
  "Kiwi Control.app",
  "Contents",
  "MacOS",
  "sj-ui"
);
const cliEntrypoint = path.join(repoRoot, "packages", "sj-cli", "dist", "cli.js");
const probeTimeoutMs = 60_000;
const probeMaxAttempts = 2;
const probeRetryDelayMs = 1_000;

async function main() {
  if (process.platform !== "darwin") {
    throw new Error("Rendered desktop verification currently supports macOS bundle launches only.");
  }

  await fs.access(desktopExecutable).catch(() => {
    throw new Error(`Built desktop executable not found at ${desktopExecutable}. Run npm run ui:desktop:build first.`);
  });
  await fs.access(cliEntrypoint).catch(() => {
    throw new Error(`Built CLI entrypoint not found at ${cliEntrypoint}. Run npm run build first.`);
  });

  await killExistingKiwiProcesses();

  const blockedRepo = await createBlockedRepo();
  try {
    await verifyOverviewBlockedState(blockedRepo);
    await verifyRailNavigationBehavior(blockedRepo);
    await verifyHistoryDrawerTruth();
    await verifyMachineView(blockedRepo);
    await verifyPackSelectionView();
  } finally {
    await killExistingKiwiProcesses();
  }

  console.log("Rendered desktop verification passed.");
}

async function createBlockedRepo() {
  const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "kiwi-rendered-desktop-"));
  const targetRoot = path.join(tempDir, "blocked-repo");
  await fs.mkdir(targetRoot, { recursive: true });
  await fs.writeFile(path.join(targetRoot, "package.json"), '{\n  "name": "blocked-repo"\n}\n', "utf8");

  const config = await loadCanonicalConfig(repoRoot);
  await bootstrapTarget(
    {
      repoRoot,
      targetRoot
    },
    config
  );

  await failWorkflowStep(targetRoot, {
    task: "stabilize product surface launch semantics",
    stepId: "generate-run-packets",
    failureReason: "Run packets could not be generated for the current repo guidance state.",
    validation: "Generate run packets before execution can continue."
  });

  return {
    tempDir,
    targetRoot
  };
}

async function verifyOverviewBlockedState(repo) {
  const snapshot = await launchAndCollectProbeWithRetry(repo.targetRoot, null, (payload) =>
    payload.activeView === "overview"
      && payload.activeMode === "execution"
      && payload.targetRoot === repo.targetRoot
      && payload.repoMode === "healthy"
      && typeof payload.aiSetupDetected === "boolean"
      && typeof payload.machineSetupStatus === "string"
      && includesAll(payload.visibleSections, [
        "overview-primary-hero",
        "blocked-workflow-fix",
        "explain-selection"
      ])
      && !payload.visibleSections.includes("command-banner")
      && payload.inspectorOpen === false
      && includesAll(payload.visibleCommands, ["guide", "next", "validate"])
  );

  assert.equal(snapshot.activeView, "overview");
  assert.equal(snapshot.repoMode, "healthy");
  assert.equal(typeof snapshot.executionRevision, "number");
  assert.equal(typeof snapshot.aiSetupDetected, "boolean");
  assert.match(snapshot.machineSetupStatus, /ready|needs-work|stale/);
  assert.equal(snapshot.inspectorOpen, false);
  assert.equal(snapshot.visibleSections.includes("overview-primary-hero"), true);
  assert.equal(snapshot.visibleSections.includes("blocked-workflow-fix"), true);
  assert.equal(snapshot.visibleSections.includes("explain-selection"), true);
  assert.equal(snapshot.visibleSections.includes("command-banner"), false);
}

async function verifyMachineView(repo) {
  const snapshot = await launchAndCollectProbeWithRetry(repo.targetRoot, "machine", (payload) =>
    payload.activeView === "machine"
      && payload.targetRoot === repo.targetRoot
      && payload.inspectorOpen === false
      && typeof payload.aiSetupDetected === "boolean"
      && !payload.visibleSections.includes("overview-primary-hero")
      && !payload.visibleSections.includes("command-banner")
      && payload.visibleSections.includes("machine-setup-readiness")
  );

  assert.equal(snapshot.activeView, "machine");
  assert.equal(snapshot.inspectorOpen, false);
  assert.equal(snapshot.visibleSections.includes("machine-setup-readiness"), true);
}

async function verifyRailNavigationBehavior(repo) {
  await killExistingKiwiProcesses();
  const session = await launchProbeSession(repo.targetRoot, "overview");
  try {
    const initial = await session.waitForPayload((payload) =>
      payload.activeView === "overview"
      && payload.activeRailView === "overview"
      && payload.activeRailAriaCurrent === true
      && typeof payload.railButtonCount === "number"
      && payload.railButtonCount >= 6
    );

    await session.writeRenderAction({ actionType: "set-rail-scroll", y: 48 });
    const afterScroll = await session.waitForPayload((payload) =>
      payload.activeView === "overview"
      && typeof payload.railScrollTop === "number"
      && payload.railScrollTop >= 40
    );

    await session.writeRenderAction({ actionType: "switch-mode", mode: "inspection" });
    const afterModeSwitch = await session.waitForPayload((payload) =>
      payload.activeMode === "inspection"
      && payload.activeView === "overview"
      && payload.activeRailView === "overview"
      && typeof payload.railScrollTop === "number"
      && payload.railScrollTop >= afterScroll.railScrollTop - 2
    );

    await session.writeRenderAction({ actionType: "click-view", view: "graph" });
    const afterGraphClick = await session.waitForPayload((payload) =>
      payload.activeView === "graph"
      && payload.activeRailView === "graph"
      && payload.activeRailAriaCurrent === true
      && payload.graphSurfaceVisible === true
      && typeof payload.railScrollTop === "number"
      && payload.railScrollTop >= afterScroll.railScrollTop - 2
    );

    await session.writeRenderAction({ actionType: "click-view", view: "mcps" });
    const afterMcpsClick = await session.waitForPayload((payload) =>
      payload.activeView === "mcps"
      && payload.activeRailView === "mcps"
      && payload.activeRailAriaCurrent === true
      && includesAll(payload.visibleSections, [
        "mcp-selected-pack",
        "mcp-selectable-packs",
        "mcp-blocked-packs"
      ])
    );

    assert.equal(initial.activeRailView, "overview");
    assert.equal(afterModeSwitch.activeMode, "inspection");
    assert.equal(afterGraphClick.graphSurfaceVisible, true);
    assert.equal(afterMcpsClick.activeRailView, "mcps");
  } finally {
    await session.dispose();
    await killExistingKiwiProcesses();
  }
}

async function verifyHistoryDrawerTruth() {
  const snapshot = await launchAndCollectProbeWithRetry(repoRoot, null, (payload) =>
    payload.activeView === "overview"
      && payload.targetRoot === repoRoot
      && typeof payload.historyLineCount === "number"
      && payload.historyLineCount > 0
  );

  assert.equal(snapshot.historyLineCount > 0, true);
}

async function verifyPackSelectionView() {
  const originalPack = runCliJson(["pack", "status", "--json", "--target", repoRoot]);
  const restoreArgs = originalPack.explicitSelection
    ? ["pack", "set", originalPack.explicitSelection, "--json", "--target", repoRoot]
    : ["pack", "clear", "--json", "--target", repoRoot];
  await killExistingKiwiProcesses();
  const session = await launchProbeSession(repoRoot, "mcps");
  try {
    const before = await session.waitForPayload((payload) =>
      payload.activeView === "mcps"
      && payload.activeMode === "execution"
      && payload.targetRoot === repoRoot
      && payload.inspectorOpen === false
      && typeof payload.selectedPack === "string"
      && Array.isArray(payload.selectablePackIds)
      && payload.selectablePackIds.includes("web-qa-pack")
      && !payload.selectablePackIds.includes("aws-pack")
      && !payload.selectablePackIds.includes("ios-pack")
      && !payload.selectablePackIds.includes("android-pack")
      && Array.isArray(payload.packCatalog)
      && payload.packCatalog.some((entry) => entry.id === "aws-pack" && entry.executable === false && typeof entry.unavailablePackReason === "string")
      && payload.packCatalog.some((entry) => entry.id === "ios-pack" && entry.executable === false && typeof entry.unavailablePackReason === "string")
    );

    await session.writeRenderAction({ actionType: "click-pack", packId: "web-qa-pack" });
    const afterSelect = await session.waitForPayload((payload) =>
      payload.selectedPack === "web-qa-pack"
      && payload.selectedPackSource === "runtime-explicit"
      && payload.executionRevision > before.executionRevision
      && !payload.selectablePackIds.includes("web-qa-pack")
    );

    await session.writeRenderAction({ actionType: "click-pack", packId: "web-qa-pack" });
    const afterRepeat = await session.waitForPayload((payload) =>
      payload.selectedPack === "web-qa-pack"
      && payload.executionRevision === afterSelect.executionRevision
    );

    await session.writeRenderAction({ actionType: "clear-pack" });
    const afterClear = await session.waitForPayload((payload) =>
      payload.selectedPackSource === "heuristic-default"
      && payload.executionRevision > afterSelect.executionRevision
    );

    await session.writeRenderAction({ actionType: "clear-pack" });
    const afterRepeatClear = await session.waitForPayload((payload) =>
      payload.selectedPackSource === "heuristic-default"
      && payload.executionRevision === afterClear.executionRevision
    );

    assert.equal(before.selectablePackIds.includes("web-qa-pack"), true);
    assert.equal(before.inspectorOpen, false);
    assert.equal(afterSelect.selectedPack, "web-qa-pack");
    assert.equal(afterSelect.selectedPackSource, "runtime-explicit");
    assert.equal(afterRepeat.executionRevision, afterSelect.executionRevision);
    assert.equal(afterClear.selectedPackSource, "heuristic-default");
    assert.equal(afterRepeatClear.executionRevision, afterClear.executionRevision);
  } finally {
    runCliJson(restoreArgs, true);
    await session.dispose();
    await killExistingKiwiProcesses();
  }
}

async function launchProbeSession(targetRoot, probeView) {
  const bridgeDir = await fs.mkdtemp(path.join(os.tmpdir(), "kiwi-render-bridge-"));
  const probeFile = path.join(bridgeDir, "render-probe.json");
  const actionFile = path.join(bridgeDir, "render-action.json");
  const launchStatusPath = path.join(bridgeDir, "desktop-launch-status.json");
  const launchLogPath = path.join(bridgeDir, "desktop-launch-log.json");
  const child = spawn(process.execPath, [cliEntrypoint, "ui", "--target", targetRoot], {
    cwd: repoRoot,
    env: {
      ...process.env,
      KIWI_CONTROL_DESKTOP_BRIDGE_DIR: bridgeDir,
      KIWI_CONTROL_DESKTOP: desktopExecutable,
      KIWI_CONTROL_RENDER_PROBE_FILE: probeFile,
      KIWI_CONTROL_RENDER_ACTION_FILE: actionFile,
      ...(probeView ? { KIWI_CONTROL_RENDER_PROBE_VIEW: probeView } : {})
    },
    stdio: ["ignore", "pipe", "pipe"]
  });

  let lastPayload = null;
  let childStdout = "";
  let childStderr = "";
  child.stdout.on("data", (chunk) => {
    childStdout += chunk.toString();
  });
  child.stderr.on("data", (chunk) => {
    childStderr += chunk.toString();
  });

  return {
    async waitForPayload(predicate) {
      const deadline = Date.now() + probeTimeoutMs;
      while (Date.now() < deadline) {
        await delay(100);
        const payload = await readJsonFile(probeFile);
        if (!payload) {
          continue;
        }
        lastPayload = payload;
        if (predicate(payload)) {
          return payload;
        }
      }

      const launchStatus = await readTextIfExists(launchStatusPath);
      const launchLog = await readTextIfExists(launchLogPath);
      throw new Error(
        [
          `Rendered desktop probe timed out for ${targetRoot}${probeView ? ` view=${probeView}` : ""}.`,
          childStdout ? `Child stdout: ${childStdout}` : "Child stdout: <empty>",
          childStderr ? `Child stderr: ${childStderr}` : "Child stderr: <empty>",
          `Last payload: ${JSON.stringify(lastPayload, null, 2)}`,
          launchStatus ? `Launch status: ${launchStatus}` : "Launch status: <missing>",
          launchLog ? `Launch log: ${launchLog}` : "Launch log: <missing>"
        ].join("\n")
      );
    },
    async writeRenderAction(payload) {
      await fs.writeFile(actionFile, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
    },
    async dispose() {
      child.kill("SIGTERM");
      await waitForExit(child, 1500);
      if (child.exitCode === null && child.signalCode === null) {
        child.kill("SIGKILL");
      }
    }
  };
}

async function launchAndCollectProbeWithRetry(targetRoot, probeView, predicate) {
  let lastError = null;
  for (let attempt = 1; attempt <= probeMaxAttempts; attempt += 1) {
    try {
      await killExistingKiwiProcesses();
      return await launchAndCollectProbe(targetRoot, probeView, predicate);
    } catch (error) {
      lastError = error;
      if (attempt >= probeMaxAttempts) {
        throw error;
      }
      await killExistingKiwiProcesses();
      await delay(probeRetryDelayMs);
    }
  }

  throw lastError ?? new Error("Rendered desktop probe failed without an error.");
}

async function launchAndCollectProbe(targetRoot, probeView, predicate) {
  const bridgeDir = await fs.mkdtemp(path.join(os.tmpdir(), "kiwi-render-bridge-"));
  const probeFile = path.join(bridgeDir, "render-probe.json");
  const launchStatusPath = path.join(bridgeDir, "desktop-launch-status.json");
  const launchLogPath = path.join(bridgeDir, "desktop-launch-log.json");
  const child = spawn(process.execPath, [cliEntrypoint, "ui", "--target", targetRoot], {
    cwd: repoRoot,
    env: {
      ...process.env,
      KIWI_CONTROL_DESKTOP_BRIDGE_DIR: bridgeDir,
      KIWI_CONTROL_DESKTOP: desktopExecutable,
      KIWI_CONTROL_RENDER_PROBE_FILE: probeFile,
      ...(probeView ? { KIWI_CONTROL_RENDER_PROBE_VIEW: probeView } : {})
    },
    stdio: ["ignore", "pipe", "pipe"]
  });

  let lastPayload = null;
  let childStdout = "";
  let childStderr = "";
  let childError = null;
  let childExit = null;
  child.stdout.on("data", (chunk) => {
    childStdout += chunk.toString();
  });
  child.stderr.on("data", (chunk) => {
    childStderr += chunk.toString();
  });
  child.once("error", (error) => {
    childError = error instanceof Error ? error.message : String(error);
  });
  child.once("exit", (code, signal) => {
    childExit = { code, signal };
  });
  try {
    const deadline = Date.now() + probeTimeoutMs;
    while (Date.now() < deadline) {
      await delay(100);
      const payload = await readJsonFile(probeFile);
      if (!payload) {
        continue;
      }
      lastPayload = payload;
      if (predicate(payload)) {
        return payload;
      }
    }

    const launchStatus = await readTextIfExists(launchStatusPath);
    const launchLog = await readTextIfExists(launchLogPath);
    throw new Error(
      [
        `Rendered desktop probe timed out for ${targetRoot}${probeView ? ` view=${probeView}` : ""}.`,
        childError ? `Child error: ${childError}` : "Child error: <none>",
        childExit ? `Child exit: ${JSON.stringify(childExit)}` : "Child exit: <still running or not observed>",
        childStdout ? `Child stdout: ${childStdout}` : "Child stdout: <empty>",
        childStderr ? `Child stderr: ${childStderr}` : "Child stderr: <empty>",
        `Last payload: ${JSON.stringify(lastPayload, null, 2)}`,
        launchStatus ? `Launch status: ${launchStatus}` : "Launch status: <missing>",
        launchLog ? `Launch log: ${launchLog}` : "Launch log: <missing>"
      ].join("\n")
    );
  } finally {
    child.kill("SIGTERM");
    await waitForExit(child, 1500);
    if (child.exitCode === null && child.signalCode === null) {
      child.kill("SIGKILL");
    }
  }
}

async function waitForExit(child, timeoutMs) {
  await Promise.race([
    new Promise((resolve) => child.once("exit", resolve)),
    delay(timeoutMs)
  ]);
}

async function killExistingKiwiProcesses() {
  for (const pattern of [
    "/Applications/Kiwi Control.app/Contents/MacOS/sj-ui",
    "/Volumes/shrey ssd/shrey-junior/apps/sj-ui/src-tauri/target/release/bundle/macos/Kiwi Control.app/Contents/MacOS/sj-ui",
    "kiwi-control-tauri-target/release/sj-ui"
  ]) {
    spawnSync("pkill", ["-f", pattern], { stdio: "ignore" });
  }
  await delay(400);
}

async function readJsonFile(filePath) {
  try {
    return JSON.parse(await fs.readFile(filePath, "utf8"));
  } catch {
    return null;
  }
}

async function readTextIfExists(filePath) {
  try {
    return await fs.readFile(filePath, "utf8");
  } catch {
    return "";
  }
}

function includesAll(values, required) {
  return required.every((value) => values.includes(value));
}

function delay(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

function runCliJson(args, allowFailure = false) {
  const result = spawnSync(process.execPath, [cliEntrypoint, ...args], {
    cwd: repoRoot,
    encoding: "utf8"
  });
  if (result.error) {
    throw result.error;
  }
  if (!allowFailure && (result.status ?? 1) !== 0) {
    throw new Error(result.stderr || result.stdout || `CLI command failed: ${args.join(" ")}`);
  }
  return result.stdout ? JSON.parse(result.stdout) : null;
}

main().catch((error) => {
  console.error(error instanceof Error ? error.stack ?? error.message : String(error));
  process.exitCode = 1;
});
