import assert from "node:assert/strict";
import test from "node:test";
import { createDeploymentController } from "./controller.mjs";
import { redactSensitiveText } from "./config.mjs";
import { formatDeploymentFailure } from "./cli.mjs";

function createFakeDeployDeps(overrides = {}) {
  const calls = [];
  const storage = { sentinel: Buffer.from("storage-sentinel-bytes") };
  let running = Boolean(overrides.running);
  let statusState = { process: "stopped", apiHealth: "unhealthy", apiReady: "unhealthy", web: "unreachable", ok: false };

  const deps = {
    calls,
    storage,
    async loadConfiguration() {
      calls.push("load-config");
      return { env: { WEB_PORT: "3000", API_PORT: "8787" }, envText: "", webText: "" };
    },
    async preflight(_config, options) {
      calls.push("preflight");
      if (options?.command === "start" && overrides.missingBuild) {
        throw new Error("请先运行 ./deploy.sh deploy");
      }
    },
    async configure(config) {
      calls.push("configure");
      return config;
    },
    async checkDependencies() {
      calls.push("check-dependencies");
    },
    async selectPorts(config) {
      calls.push("select-ports");
      return config;
    },
    async writeConfiguration() {
      calls.push("write-config");
    },
    async isRunning() {
      return running;
    },
    async stop() {
      calls.push("stop");
      running = false;
      statusState = { ...statusState, process: "stopped", ok: false };
    },
    async installProject() {
      calls.push("install");
    },
    async buildTypeScript() {
      calls.push("build-typescript");
      if (overrides.failBuild) {
        const error = new Error("TypeScript build failed");
        error.logPath = "storage/logs/deploy-20260722-143000.log";
        throw error;
      }
    },
    async buildWeb() {
      calls.push("build-web");
      if (overrides.failWebBuild) {
        const error = new Error("Web build failed");
        error.logPath = "storage/logs/deploy-20260722-143000.log";
        throw error;
      }
    },
    async buildTui() {
      calls.push("build-tui");
      if (overrides.failTuiBuild) {
        const error = new Error("TUI build failed");
        error.logPath = "storage/logs/deploy-20260722-143000.log";
        throw error;
      }
    },
    async ensureTuiReady() {
      calls.push("ensure-tui-ready");
    },
    async checkApiForTui() {
      calls.push("check-api-for-tui");
      if (overrides.tuiApiUnhealthy) {
        throw new Error("API is not healthy at http://127.0.0.1:8787/healthz");
      }
    },
    async startTui() {
      calls.push("start-tui");
    },

    async verifyPorts() {
      calls.push("verify-ports-again");
      if (overrides.portConflictAfterBuild) {
        throw new Error("Web port 3000 is already in use");
      }
    },
    async start() {
      calls.push("start");
      running = true;
      statusState = {
        process: "running",
        apiHealth: "healthy",
        apiReady: "ready",
        web: "reachable",
        ok: true
      };
    },
    async waitForHealth() {
      calls.push("wait-for-health");
    },
    async markHealthy() {
      calls.push("mark-healthy");
    },
    async status() {
      calls.push("status");
      return statusState;
    },
    async logs() {
      calls.push("logs");
    },
    async doctor() {
      calls.push("doctor");
    },
    async printHelp() {
      calls.push("help");
    },
    async reportFailure(failure) {
      calls.push("report-failure");
      deps.lastFailure = failure;
    },
    ...overrides.deps
  };
  return deps;
}

test("fresh deploy runs configure through healthy state", async () => {
  const deps = createFakeDeployDeps({ running: false });
  const controller = createDeploymentController(deps);
  const result = await controller.run({ command: "deploy", reconfigure: false, nonInteractive: true });
  assert.ok(result.ok);
  assert.deepEqual(result.calls, [
    "load-config",
    "preflight",
    "configure",
    "check-dependencies",
    "select-ports",
    "write-config",
    "install",
    "build-typescript",
    "build-web",
    "build-tui",
    "verify-ports-again",
    "start",
    "wait-for-health",
    "mark-healthy"
  ]);
  assert.ok(!result.calls.includes("stop-old"));
  assert.ok(!result.calls.includes("start-tui"));
});

test("update deploy stops only after preflight and before install/build", async () => {
  const deps = createFakeDeployDeps({ running: true });
  const controller = createDeploymentController(deps);
  const result = await controller.run({ command: "deploy", reconfigure: false, nonInteractive: true });
  const stopIndex = result.calls.indexOf("stop-old");
  const installIndex = result.calls.indexOf("install");
  assert.ok(stopIndex > result.calls.indexOf("preflight"));
  assert.ok(stopIndex > result.calls.indexOf("write-config"));
  assert.ok(installIndex > stopIndex);
  assert.ok(result.calls.indexOf("build-typescript") > stopIndex);
});

test("build failure keeps storage intact and reports maintenance window", async () => {
  const deps = createFakeDeployDeps({ running: true, failWebBuild: true });
  const controller = createDeploymentController(deps);
  await assert.rejects(
    () => controller.run({ command: "deploy", reconfigure: false, nonInteractive: true }),
    /Web build failed/
  );
  assert.equal(deps.storage.sentinel.toString(), "storage-sentinel-bytes");
  assert.equal(deps.lastFailure.maintenanceWindow, true);
  assert.equal(deps.lastFailure.oldServiceRunning, false);
  assert.equal(deps.lastFailure.configurationChanged, true);
  assert.equal(deps.lastFailure.stage, "build-web");
  assert.equal(deps.lastFailure.retryCommand, "./deploy.sh deploy");
  assert.equal(deps.lastFailure.doctorCommand, "./deploy.sh doctor");
  assert.ok(!deps.calls.includes("start"));
  const formatted = formatDeploymentFailure({
    ...deps.lastFailure,
    summary: "Web build failed"
  });
  assert.match(formatted, /现有数据未被修改/);
  assert.match(formatted, /维护窗口/);
  assert.doesNotMatch(formatted, /已恢复|rolled back|restored/i);
});

test("port conflict after build prevents start and never kills listeners", async () => {
  const deps = createFakeDeployDeps({ running: false, portConflictAfterBuild: true });
  let killed = false;
  deps.killListener = () => {
    killed = true;
  };
  const controller = createDeploymentController(deps);
  await assert.rejects(
    () => controller.run({ command: "deploy", reconfigure: false, nonInteractive: true }),
    /already in use/
  );
  assert.ok(!deps.calls.includes("start"));
  assert.equal(killed, false);
});

test("start with missing build artifact fails with deploy hint", async () => {
  const deps = createFakeDeployDeps({ missingBuild: true });
  const controller = createDeploymentController(deps);
  await assert.rejects(() => controller.run({ command: "start" }), /请先运行 \.\/deploy\.sh deploy/);
  assert.ok(!deps.calls.includes("install"));
  assert.ok(!deps.calls.includes("build-typescript"));
});

test("start with incomplete config fails with deploy hint and never starts", async () => {
  const deps = createFakeDeployDeps();
  deps.preflight = async (_config, options) => {
    deps.calls.push("preflight");
    if (options?.command === "start") {
      throw new Error("缺少合法完整的 .env 配置,请先运行 ./deploy.sh deploy");
    }
  };
  const controller = createDeploymentController(deps);
  await assert.rejects(() => controller.run({ command: "start" }), /缺少合法完整的 \.env|请先运行 \.\/deploy\.sh deploy/);
  assert.ok(!deps.calls.includes("start"));
});

test("status reports stopped and unhealthy components", async () => {
  const deps = createFakeDeployDeps();
  deps.status = async () => ({
    process: "stopped",
    apiHealth: "unhealthy",
    apiReady: "unhealthy",
    web: "unreachable",
    ok: false
  });
  const controller = createDeploymentController(deps);
  const summary = await controller.run({ command: "status" });
  assert.equal(summary.process, "stopped");
  assert.equal(summary.apiHealth, "unhealthy");
  assert.equal(summary.web, "unreachable");
});

test("restart does not install or build", async () => {
  const deps = createFakeDeployDeps({ running: true });
  const controller = createDeploymentController(deps);
  await controller.run({ command: "restart" });
  assert.ok(deps.calls.includes("stop"));
  assert.ok(deps.calls.includes("start"));
  assert.ok(!deps.calls.includes("install"));
  assert.ok(!deps.calls.includes("build-typescript"));
  assert.ok(!deps.calls.includes("build-web"));
});

test("stop twice succeeds both times", async () => {
  const deps = createFakeDeployDeps({ running: true });
  const controller = createDeploymentController(deps);
  await controller.run({ command: "stop" });
  await controller.run({ command: "stop" });
  assert.equal(deps.calls.filter((call) => call === "stop").length, 2);
});

test("runtime and deploy logs redact fixture secrets", () => {
  const text = redactSensitiveText(
    "AUTH_SESSION_SECRET=fixture-deploy-secret-at-least-32-chars\nLLM_API_KEY=sk-test\nWEB_PORT=3000\n"
  );
  assert.doesNotMatch(text, /fixture-deploy-secret-at-least-32-chars/);
  assert.doesNotMatch(text, /sk-test/);
  assert.match(text, /WEB_PORT=3000/);
});

test("deploy failure uses beginDeployLog path", async () => {
  let failure;
  const controller = createDeploymentController({
    async beginDeployLog() {
      return "storage/logs/deploy-20260722143000.log";
    },
    async loadConfiguration() {
      return { env: {} };
    },
    async preflight() {},
    async configure(config) {
      return config;
    },
    async checkDependencies() {},
    async selectPorts(config) {
      return config;
    },
    async writeConfiguration() {},
    async isRunning() {
      return false;
    },
    async stop() {},
    async installProject() {},
    async buildTypeScript() {
      throw new Error("TypeScript build failed");
    },
    async buildWeb() {},
    async buildTui() {},
    async verifyPorts() {},
    async start() {},
    async waitForHealth() {},
    async markHealthy() {},
    async reportFailure(details) {
      failure = details;
    }
  });

  await assert.rejects(
    () => controller.run({ command: "deploy", reconfigure: false, nonInteractive: true }),
    /TypeScript build failed/
  );
  assert.equal(failure.logPath, "storage/logs/deploy-20260722143000.log");
});

test("deploy never auto-starts TUI client", async () => {
  const deps = createFakeDeployDeps({ running: false });
  const controller = createDeploymentController(deps);
  const result = await controller.run({ command: "deploy", reconfigure: false, nonInteractive: true });
  assert.ok(result.calls.includes("build-tui"));
  assert.ok(!result.calls.includes("ensure-tui-ready"));
  assert.ok(!result.calls.includes("check-api-for-tui"));
  assert.ok(!result.calls.includes("start-tui"));
});

test("optional tui command requires healthy API before start", async () => {
  const deps = createFakeDeployDeps({ tuiApiUnhealthy: true });
  const controller = createDeploymentController(deps);
  await assert.rejects(() => controller.run({ command: "tui", runtimeUrl: null }), /API is not healthy/);
  assert.ok(deps.calls.includes("ensure-tui-ready"));
  assert.ok(deps.calls.includes("check-api-for-tui"));
  assert.ok(!deps.calls.includes("start-tui"));
});