import assert from "node:assert/strict";
import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { spawnSync } from "node:child_process";
import { test } from "bun:test";

import { withWebObserveCollectRendered } from "../hwlab-node-web-observe-render";
import { nodeWebObserveAnalyzerSource } from "../hwlab-node-web-observe-analyzer-source";
import { nodeWebObserveCollectViewNodeScript } from "../hwlab-node-web-observe-collect";
import { nodeWebObserveCollectNodeScript } from "./web-observe-scripts";

const alertThresholds = {
  sameOriginApiSlowMs: 60000,
  partialApiSlowMs: 60000,
  longLivedStreamOpenSlowMs: 60000,
  visibleLoadingSlowMs: 60000,
  turnTimingSampleSlackSeconds: 60,
  turnElapsedSevereTimeoutSeconds: 3600,
  domEvaluateTimeoutRedCount: 99,
  domEvaluateTimeoutRedWindowMs: 60000,
  screenshotTimeoutRedCount: 99,
  pageErrorRedCount: 99,
  longTaskRedMs: 1000,
  longAnimationFrameRedMs: 1000,
  eventLoopGapRedMs: 1000,
  browserProcessSampleIntervalMs: 1000,
  requestRateBucketMs: 10000,
  requestRateTotalRedPerMinute: 999999,
  requestRatePageRedPerMinute: 999999,
  requestRateApiPathRedPerMinute: 999999,
  browserTotalRssRedMb: 999999,
  browserProcessRssRedMb: 999999,
  browserRssGrowthRedMb: 999999,
  browserRssGrowthWindowMs: 60000,
  playwrightResponsivenessRedMs: 60000,
  playwrightResponsivenessTimeoutRedCount: 99,
  cdpMetricsTimeoutRedCount: 99,
  uncommandedStateChangeCommandWindowMs: 1000,
  scrollJumpCommandWindowMs: 1000,
  scrollJumpFromY: 999999,
  scrollJumpToY: 999999,
  sessionRailFallbackRatio: 0.5,
};

const browserFreezePolicy = {
  enabled: true,
  blockerWindowMs: 60000,
  memory: { totalRssBlockerMb: 999999, processRssBlockerMb: 999999, growthBlockerMb: 999999 },
  responsiveness: { latencyBlockerMs: 60000, eventBlockerCount: 99 },
  cdp: { metricsTimeoutBlockerCount: 99 },
  kill: { enabled: false, gracefulSignal: "SIGTERM", forceSignal: "SIGKILL", graceMs: 1000, pollIntervalMs: 100, exitCode: 124 },
};

function shellQuote(value: string): string {
  return `'${value.replace(/'/g, `'\\''`)}'`;
}

async function writeJsonl(path: string, rows: Array<Record<string, unknown>>): Promise<void> {
  await writeFile(path, rows.map((row) => JSON.stringify(row)).join("\n") + (rows.length > 0 ? "\n" : ""));
}

async function runAnalyzer(stateDir: string): Promise<Record<string, unknown>> {
  const analyzerPath = join(stateDir, "analyze.mjs");
  await writeFile(analyzerPath, nodeWebObserveAnalyzerSource(), { mode: 0o700 });
  const result = spawnSync("bun", [analyzerPath, stateDir], {
    cwd: join(import.meta.dir, "../../.."),
    env: {
      ...process.env,
      UNIDESK_WEB_OBSERVE_ANALYZE_TAIL_SAMPLES: "0",
      UNIDESK_WEB_OBSERVE_ALERT_THRESHOLDS_JSON: JSON.stringify(alertThresholds),
      UNIDESK_WEB_OBSERVE_BROWSER_FREEZE_POLICY_JSON: JSON.stringify(browserFreezePolicy),
    },
    encoding: "utf8",
  });
  assert.equal(result.status, 0, result.stderr || result.stdout);
  return JSON.parse(await readFile(join(stateDir, "analysis", "report.json"), "utf8")) as Record<string, unknown>;
}

async function writeState(): Promise<string> {
  const stateDir = await mkdtemp(join(tmpdir(), "unidesk-web-observe-web-performance-"));
  await mkdir(join(stateDir, "analysis"), { recursive: true });
  await writeFile(join(stateDir, "manifest.json"), JSON.stringify({ jobId: "webobs-web-performance-test", status: "completed" }) + "\n");
  await writeFile(join(stateDir, "heartbeat.json"), JSON.stringify({ status: "completed", updatedAt: "2026-07-02T17:00:05.000Z" }) + "\n");
  await writeJsonl(join(stateDir, "samples.jsonl"), []);
  await writeJsonl(join(stateDir, "console.jsonl"), []);
  await writeJsonl(join(stateDir, "errors.jsonl"), []);
  await writeJsonl(join(stateDir, "browser-process.jsonl"), []);
  await writeJsonl(join(stateDir, "performance-events.jsonl"), []);
  await writeJsonl(join(stateDir, "artifacts.jsonl"), []);
  await writeJsonl(join(stateDir, "control.jsonl"), [{
    type: "sendPrompt",
    phase: "completed",
    commandId: "cmd-prompt",
    ts: "2026-07-02T17:00:00.000Z",
  }]);
  await writeJsonl(join(stateDir, "network.jsonl"), [
    {
      type: "request",
      ts: "2026-07-02T17:00:01.000Z",
      pageRole: "control",
      pageId: "page-1",
      method: "POST",
      url: "https://hwlab.example.test/v1/web-performance",
      resourceType: "fetch",
      webPerformancePayload: {
        captureStatus: "captured",
        parseStatus: "parsed",
        schemaVersion: "hwlab-web-performance-v2",
        byteCount: 768,
        byteLimit: 65536,
        bodyHash: "sha256:payload",
        eventCount: 1,
        storedEventCount: 1,
        events: [{
          ts: "2026-07-02T17:00:01.000Z",
          eventType: "runtime_diagnostic",
          diagnosticCode: "workbench_sse_flush",
          reason: "sse_flush",
          module: "workbench-runtime",
          traceId: "trc_webperf_fixture",
          sessionIdHash: "sha256:session",
          eventCount: 12,
          deliveredCount: 10,
          chunkCount: 3,
          flushDurationMs: 18.75,
          droppedCount: 2,
          maxItemsPerChunk: 5,
          maxChunkMs: 7.5,
          replacedByKey: 3,
          valuesRedacted: true,
        }],
        valuesRedacted: true,
      },
    },
    {
      type: "request",
      ts: "2026-07-02T17:00:02.000Z",
      method: "POST",
      url: "https://hwlab.example.test/v1/web-performance",
      webPerformancePayload: {
        captureStatus: "captured",
        parseStatus: "invalid-json",
        byteCount: 12,
        byteLimit: 65536,
        bodyHash: "sha256:invalid",
        eventCount: null,
        events: [],
        valuesRedacted: true,
      },
    },
    {
      type: "request",
      ts: "2026-07-02T17:00:03.000Z",
      method: "POST",
      url: "https://hwlab.example.test/v1/web-performance",
      webPerformancePayload: {
        captureStatus: "skipped-over-limit",
        parseStatus: "not-parsed-over-limit",
        byteCount: 70000,
        byteLimit: 65536,
        bodyHash: "sha256:large",
        eventCount: null,
        events: [],
        valuesRedacted: true,
      },
    },
  ]);
  return stateDir;
}

test("generated observe analyzer source compiles as a single ESM bundle", async () => {
  const stateDir = await mkdtemp(join(tmpdir(), "unidesk-web-observe-analyzer-compile-"));
  const analyzerPath = join(stateDir, "observer-analyzer.mjs");
  await writeFile(analyzerPath, nodeWebObserveAnalyzerSource(), { mode: 0o700 });
  const result = spawnSync("node", ["--check", analyzerPath], { encoding: "utf8" });
  assert.equal(result.status, 0, result.stderr || result.stdout);
}, 20_000);

test("file collect exposes requested and resolved artifact when analysis report falls back", async () => {
  const stateDir = await mkdtemp(join(tmpdir(), "unidesk-web-observe-file-fallback-"));
  await mkdir(join(stateDir, "analysis"), { recursive: true });
  await writeFile(join(stateDir, "analysis", "analyzer-stdout.json"), JSON.stringify({
    ok: false,
    command: "web-probe-observe analyze",
    error: "fixture analyzer failed before report",
    valuesRedacted: true,
  }) + "\n");

  const script = nodeWebObserveCollectNodeScript(100, "analysis/report.json", null, null);
  const result = spawnSync("bash", ["-lc", `state_dir=${shellQuote(stateDir)}\n${script}`], {
    cwd: join(import.meta.dir, "../../.."),
    encoding: "utf8",
  });
  assert.equal(result.status, 0, result.stderr || result.stdout);
  const output = JSON.parse(result.stdout) as Record<string, unknown>;
  const file = output.file as Record<string, unknown>;
  assert.equal(output.requestedFile, "analysis/report.json");
  assert.equal(output.resolvedFile, "analysis/analyzer-stdout.json");
  assert.equal(output.fallbackReason, "requested-file-missing");
  assert.equal(file.requestedRelative, "analysis/report.json");
  assert.equal(file.resolvedRelative, "analysis/analyzer-stdout.json");

  const rendered = withWebObserveCollectRendered({
    ok: true,
    status: "collected",
    command: "web-probe observe collect",
    id: "webobs-fallback-test",
    node: "JD01",
    lane: "v03",
    view: "files",
    requestedFile: "analysis/report.json",
    collect: output,
    valuesRedacted: true,
  });
  assert.match(rendered.renderedText, /File resolution/u);
  assert.match(rendered.renderedText, /analysis\/report\.json/u);
  assert.match(rendered.renderedText, /analysis\/analyzer-stdout\.json/u);
  assert.match(rendered.renderedText, /requested-file-missing/u);
}, 20_000);

test("analyzer and performance-summary include bounded web-performance runtime diagnostics", async () => {
  const stateDir = await writeState();
  const report = await runAnalyzer(stateDir);
  const runtimeAlerts = report.runtimeAlerts as Record<string, unknown>;
  const summary = runtimeAlerts.summary as Record<string, unknown>;
  assert.equal(summary.webPerformancePayloadRequestCount, 3);
  assert.equal(summary.webPerformancePayloadParsedCount, 1);
  assert.equal(summary.webPerformancePayloadParseIssueCount, 2);
  assert.equal(summary.webPerformanceRuntimeDiagnosticCount, 1);

  const groups = runtimeAlerts.webPerformanceRuntimeDiagnosticsByCode as Array<Record<string, unknown>>;
  assert.equal(groups[0].diagnosticCode, "workbench_sse_flush");
  assert.equal(groups[0].reason, "sse_flush");
  assert.equal(groups[0].module, "workbench-runtime");
  assert.equal(groups[0].eventCount, 12);
  assert.equal(groups[0].deliveredCount, 10);
  assert.equal(groups[0].chunkCount, 3);
  assert.equal(groups[0].maxFlushDurationMs, 18.75);
  assert.equal(groups[0].replacedByKeyCount, 3);

  const reportText = JSON.stringify(report);
  assert.match(reportText, /workbench_sse_flush/u);
  assert.doesNotMatch(reportText, /rawBody|super-secret|ses_unredacted/u);

  const script = nodeWebObserveCollectViewNodeScript({
    maxFiles: 100,
    view: "performance-summary",
    traceId: null,
    sampleSeq: null,
    timestamp: null,
    turn: null,
    commandId: null,
    windowMs: null,
  });
  const result = spawnSync("bash", ["-lc", `state_dir=${shellQuote(stateDir)}\n${script}`], {
    cwd: join(import.meta.dir, "../../.."),
    encoding: "utf8",
  });
  assert.equal(result.status, 0, result.stderr || result.stdout);
  const output = JSON.parse(result.stdout);
  const renderedText = String(output.renderedText ?? "");
  assert.match(renderedText, /Web performance runtime diagnostics/u);
  assert.match(renderedText, /payloads=3 parsed=1 parseIssues=2 events=1 groups=1/u);
  assert.match(renderedText, /workbench_sse_flush reason=sse_flush module=workbench-runtime/u);
  assert.match(renderedText, /eventCount=12 delivered=10 chunks=3 flushMax=18.75ms dropped=2/u);
  assert.match(renderedText, /payload states=invalid-json:1, not-parsed-over-limit:1, parsed:1/u);
}, 20_000);
