#!/usr/bin/env node

import assert from "node:assert/strict";
import { createHash, randomUUID } from "node:crypto";

const SECRET_TOKEN = "secret-run-token-for-smoke";
const ACCESS_DENIED_TEXT = "Доступ к модулю ограничен, обратитесь к администратору системы.";
const RUN_APP_ID_ALIASES = new Map([
  ["hub", "launcher"],
  ["nodedc-hub", "launcher"],
  ["nodedc_launcher", "launcher"],
  ["nodedc-launcher", "launcher"],
]);
const APP_ROUTING_CATALOG = [
  { appId: "launcher", appTitle: "NODE.DC Launcher", surface: "launcher", skillId: "launcher-context", appAliases: ["hub"], actionIdPrefixes: ["hub.", "launcher.", "access."], mcpServerNames: [], deniedText: ACCESS_DENIED_TEXT },
  { appId: "engine", appTitle: "NODE.DC Engine / InJoin", surface: "engine", skillId: "engine-context", mcpServerNames: ["nodedc-engine", "nodedc-agent-core"], deniedText: ACCESS_DENIED_TEXT },
  { appId: "ops", appTitle: "NODE.DC Ops / Tasker", surface: "ops", skillId: "ops-context", mcpServerNames: ["nodedc_ops_agent"], deniedText: ACCESS_DENIED_TEXT },
];

const adapter = {
  id: "ops",
  appId: "ops",
  title: "NODE.DC Ops",
};

const adapterPayload = {
  ok: true,
  appGrants: {
    ops: {
      appTitle: "NODE.DC Ops",
      surface: "ops",
      grantMode: "token-scoped-run-grants",
      context: {
        opsWorkspaceSlug: "nodedc",
        opsProjectId: "86629a11-eaff-4ad2-9f89-e5245a344fcc",
      },
      scopes: ["workspace:read", "project:read", "issue:read"],
      mcpServers: [
        {
          serverName: "nodedc ops agent",
          url: "https://ops-agents.nodedc.ru/mcp",
          required: true,
          startupTimeoutSec: 20,
          toolTimeoutSec: 60,
          httpHeaders: {
            Authorization: `Bearer ${SECRET_TOKEN}`,
            Accept: "application/json",
            "MCP-Protocol-Version": "2025-06-18",
          },
        },
      ],
    },
  },
};

const appGrants = normalizeEntitlementAdapterAppGrants(adapterPayload, adapter);
const mcpServers = runProfileMcpServersFromAppGrants({}, appGrants);
const appGrantSummary = summarizeRunAppGrants({ appGrants });
const assistantActions = {
  schemaVersion: "ai-workspace.assistant-actions.v1",
  endpoint: "/api/ai-workspace/hub/v1/assistant-relays/local-dev/actions",
  gatewayUrl: "https://ai-hub.nodedc.ru/api/ai-workspace/hub/v1/assistant-relays/local-dev/actions",
  gatewayToken: SECRET_TOKEN,
  actionIds: ["hub.access_request.list_pending", "hub.user.read_admin_summary"],
  phases: ["preview", "execute"],
};
const appCatalog = buildRunProfileAppCatalog({ appGrants, mcpServers, assistantActions });
const appAccess = summarizeRunAppAccess(appCatalog);
const launcherAdapter = { id: "launcher", appId: "launcher", title: "NODE.DC Launcher" };
const launcherGrants = normalizeEntitlementAdapterAppGrants({
  ok: true,
  appGrants: {
    hub: {
      appTitle: "NODE.DC Launcher",
      surface: "launcher",
      status: "granted",
      scopes: ["launcher:access:read", "launcher_admin_scope.any"],
    },
  },
}, launcherAdapter);
const catalogWithLauncherGrant = buildRunProfileAppCatalog({
  appGrants: { ...appGrants, ...launcherGrants },
  mcpServers,
  assistantActions,
});
const deniedByLauncher = mergeRunAppGrants(appGrants, normalizeEntitlementAdapterAppGrants({
  ok: true,
  appGrants: {
    ops: {
      appTitle: "NODE.DC Ops",
      surface: "ops",
      status: "denied",
      allowed: false,
      reason: "launcher_service_access_denied",
      mcpServers: [],
    },
  },
}, launcherAdapter));
const deniedMcpServers = runProfileMcpServersFromAppGrants({}, deniedByLauncher);
const explicitDeniedCatalog = buildRunProfileAppCatalog({
  appGrants: deniedByLauncher,
  mcpServers: deniedMcpServers,
  assistantActions,
});
const runProfile = {
  schemaVersion: "ai-workspace.run-profile.v1",
  runId: randomUUID(),
  createdAt: new Date("2026-06-13T00:00:00.000Z").toISOString(),
  owner: {
    ownerKey: "user:smoke-user",
    userId: "smoke-user",
    email: "smoke@example.test",
    role: "member",
    groups: ["engineering"],
  },
  sourceSurface: "engine",
  modeId: "ops",
  targetContexts: {
    ops: {
      opsWorkspaceSlug: "nodedc",
      opsProjectId: "86629a11-eaff-4ad2-9f89-e5245a344fcc",
    },
  },
  enabledToolPacks: ["engine", "ops", "ndc-agent-core"],
  appGrants: appGrantSummary,
  appCatalog,
  appAccess,
  toolProfile: {
    schemaVersion: "ai-workspace.tool-profile.v1",
    enabledToolPacks: ["engine", "ops", "ndc-agent-core"],
    mcpServers,
    mcpServerNames: mcpServers.map((server) => server.serverName),
    requiredMcpServerNames: mcpServers.filter((server) => server.required).map((server) => server.serverName),
    assistantActions,
  },
  diagnostics: {
    schemaVersion: "ai-workspace.run-profile.diagnostics.v1",
    dynamicProfile: true,
    sourceSurface: "engine",
    modeId: "ops",
    entitlementAdapters: {
      source: "adapters+settings",
      adapters: [{ appId: "ops", status: "ok", required: true }],
    },
    mcpServerNames: mcpServers.map((server) => server.serverName),
    appCatalogIds: appCatalog.map((app) => app.appId).sort(),
    grantedAppIds: appAccess.grantedAppIds,
    deniedAppIds: appAccess.deniedAppIds,
    notGrantedAppIds: appAccess.notGrantedAppIds,
  },
};
runProfile.diagnostics.profileHash = runProfileHash(runProfile);
const publicProfile = redactRunProfile(runProfile);

assert.deepEqual(Object.keys(appGrants), ["ops"]);
assert.equal(appGrants.ops.source, "entitlement-adapter");
assert.equal(appGrants.ops.appId, "ops");
assert.deepEqual(appGrantSummary.ops.scopes, ["workspace:read", "project:read", "issue:read"]);
assert.equal(appGrantSummary.ops.hasMcpServers, true);
assert.deepEqual(appGrantSummary.ops.mcpServerNames, ["nodedc_ops_agent"]);
assert.equal(appGrantSummary.ops.status, "granted");
assert.equal(appGrantSummary.ops.denied, false);

assert.equal(mcpServers.length, 1);
assert.equal(mcpServers[0].appId, "ops");
assert.equal(mcpServers[0].serverName, "nodedc_ops_agent");
assert.equal(mcpServers[0].required, true);
assert.equal(mcpServers[0].httpHeaders.Authorization, `Bearer ${SECRET_TOKEN}`);
assert.equal(mcpServers[0].httpHeaders.Accept, "application/json");

assert.equal(publicProfile.toolProfile.mcpServers.length, 1);
assert.equal(publicProfile.toolProfile.mcpServers[0].serverName, "nodedc_ops_agent");
assert.equal(publicProfile.toolProfile.mcpServers[0].httpHeaders.Authorization, "<redacted>");
assert.equal(publicProfile.toolProfile.mcpServers[0].httpHeaders.Accept, "<redacted>");
assert.equal(publicProfile.toolProfile.mcpServers[0].headers, undefined);
assert.equal(publicProfile.toolProfile.assistantActions.endpoint, "/api/ai-workspace/hub/v1/assistant-relays/local-dev/actions");
assert.equal(publicProfile.toolProfile.assistantActions.gatewayUrl, "https://ai-hub.nodedc.ru/api/ai-workspace/hub/v1/assistant-relays/local-dev/actions");
assert.equal(publicProfile.toolProfile.assistantActions.gatewayToken, "<redacted>");
assert.deepEqual(publicProfile.toolProfile.assistantActions.actionIds, ["hub.access_request.list_pending", "hub.user.read_admin_summary"]);
assert.equal(runProfile.appCatalog.find((app) => app.appId === "ops")?.status, "granted");
assert.equal(runProfile.appCatalog.find((app) => app.appId === "ops")?.denied, false);
assert.deepEqual(runProfile.appCatalog.find((app) => app.appId === "ops")?.mcpServerNames, ["nodedc_ops_agent"]);
assert.equal(runProfile.appCatalog.find((app) => app.appId === "launcher")?.status, "not-granted");
assert.equal(runProfile.appCatalog.find((app) => app.appId === "launcher")?.denied, false);
assert.equal(runProfile.appCatalog.find((app) => app.appId === "launcher")?.notGranted, true);
assert.equal(runProfile.appCatalog.find((app) => app.appId === "launcher")?.deniedText, null);
assert.equal(launcherGrants.launcher.appId, "launcher");
assert.equal(launcherGrants.hub, undefined);
assert.deepEqual(catalogWithLauncherGrant.find((app) => app.appId === "launcher")?.actionIds, ["hub.access_request.list_pending", "hub.user.read_admin_summary"]);
assert.equal(deniedByLauncher.ops.status, "denied");
assert.deepEqual(deniedByLauncher.ops.mcpServers, []);
assert.deepEqual(deniedMcpServers, []);
assert.equal(explicitDeniedCatalog.find((app) => app.appId === "ops")?.denied, true);
assert.equal(explicitDeniedCatalog.find((app) => app.appId === "ops")?.deniedText, ACCESS_DENIED_TEXT);
assert.deepEqual(runProfile.appAccess.grantedAppIds, ["ops"]);
assert.deepEqual(runProfile.appAccess.deniedAppIds, []);
assert.deepEqual(runProfile.appAccess.notGrantedAppIds, ["engine", "launcher"]);
assert.equal(JSON.stringify(publicProfile).includes(SECRET_TOKEN), false);
assert.match(runProfile.diagnostics.profileHash, /^[a-f0-9]{16}$/);

console.log(JSON.stringify({
  ok: true,
  checks: [
    "adapter_grant_normalized",
    "token_scoped_ops_mcp_in_run_profile",
    "assistant_action_relay_in_run_profile",
    "app_routing_catalog_in_run_profile",
    "hub_actions_route_to_launcher",
    "launcher_denial_removes_ops_mcp",
    "only_explicit_denied_apps_have_standard_text",
    "not_granted_apps_do_not_claim_denied_access",
    "public_run_profile_redacts_mcp_headers",
    "public_run_profile_redacts_assistant_action_gateway_token",
    "stable_public_profile_hash",
  ],
  mcpServerNames: runProfile.toolProfile.mcpServerNames,
  profileHash: runProfile.diagnostics.profileHash,
}, null, 2));

function normalizeEntitlementAdapterAppGrants(payload, currentAdapter) {
  const source = isPlainObject(payload?.appGrants)
    ? payload.appGrants
    : Array.isArray(payload?.appGrants)
      ? payload.appGrants
      : payload?.grant || payload?.appGrant || payload?.entitlement || payload?.entitlements || payload?.grants || payload;
  const out = {};
  if (Array.isArray(source)) {
    for (const item of source) {
      const grant = normalizeEntitlementAdapterGrant(item, currentAdapter);
      if (grant?.appId) out[grant.appId] = grant;
    }
    return out;
  }
  if (!isPlainObject(source)) return out;
  if (source.mcpServers || source.scopes || source.appId || source.app_id || source.surface) {
    const grant = normalizeEntitlementAdapterGrant(source, currentAdapter);
    if (grant?.appId) out[grant.appId] = grant;
    return out;
  }
  for (const [key, value] of Object.entries(source)) {
    const grant = normalizeEntitlementAdapterGrant(value, { ...currentAdapter, appId: normalizeKey(key) || currentAdapter.appId });
    if (grant?.appId) out[grant.appId] = grant;
  }
  return out;
}

function normalizeEntitlementAdapterGrant(value, currentAdapter) {
  if (!isPlainObject(value)) return null;
  const appId = canonicalRunAppId(value.appId || value.app_id || currentAdapter.appId);
  if (!appId) return null;
  return {
    ...value,
    appId,
    appTitle: optionalString(value.appTitle || value.app_title || value.title || currentAdapter.title),
    surface: optionalString(value.surface) || appId,
    source: "entitlement-adapter",
    adapterId: currentAdapter.id,
  };
}

function canonicalRunAppId(value) {
  const appId = normalizeKey(value);
  if (!appId) return "";
  return RUN_APP_ID_ALIASES.get(appId) || appId;
}

function normalizeRunAppGrantsObject(value) {
  const out = {};
  if (!isPlainObject(value)) return out;
  for (const [key, rawGrant] of Object.entries(value)) {
    if (!isPlainObject(rawGrant)) continue;
    const appId = canonicalRunAppId(rawGrant.appId || rawGrant.app_id || key);
    if (!appId) continue;
    const existing = isPlainObject(out[appId]) ? out[appId] : {};
    const grant = {
      ...existing,
      ...rawGrant,
      appId,
    };
    grant.mcpServers = isRunAppGrantDenied(grant)
      ? []
      : Object.hasOwn(rawGrant, "mcpServers") ? rawGrant.mcpServers : existing.mcpServers;
    out[appId] = grant;
  }
  return out;
}

function mergeRunAppGrants(base, next) {
  const out = normalizeRunAppGrantsObject(base);
  for (const [rawAppId, grant] of Object.entries(normalizeRunAppGrantsObject(next))) {
    const appId = canonicalRunAppId(grant.appId || rawAppId);
    if (!appId) continue;
    const existing = isPlainObject(out[appId]) ? out[appId] : {};
    const merged = {
      ...existing,
      ...grant,
      appId,
    };
    merged.mcpServers = isRunAppGrantDenied(merged)
      ? []
      : Object.hasOwn(grant, "mcpServers") ? grant.mcpServers : existing.mcpServers;
    out[appId] = merged;
  }
  return out;
}

function runProfileMcpServersFromAppGrants(settings, appGrantsInput = {}) {
  const servers = [];
  const metadata = isPlainObject(settings?.metadata) ? settings.metadata : {};
  collectInstallerMcpServers(servers, metadata.mcpServers, {});
  const grants = normalizeRunAppGrantsObject(appGrantsInput);
  for (const [appId, grant] of Object.entries(grants)) {
    if (!isPlainObject(grant)) continue;
    if (isRunAppGrantDenied(grant)) continue;
    collectInstallerMcpServers(servers, grant.mcpServers, {
      appId: optionalString(grant.appId) || normalizeKey(appId),
      appTitle: optionalString(grant.appTitle || grant.title),
    });
  }

  const byServerName = new Map();
  for (const server of servers.map(sanitizeInstallerMcpServer).filter(Boolean)) {
    if (server.enabled === false) continue;
    byServerName.set(server.serverName, server);
  }
  return Array.from(byServerName.values());
}

function summarizeRunAppGrants(metadata) {
  const grants = normalizeRunAppGrantsObject(metadata?.appGrants);
  const out = {};
  for (const [key, value] of Object.entries(grants)) {
    if (!isPlainObject(value)) continue;
    const appId = canonicalRunAppId(value.appId || key);
    if (!appId) continue;
    const denied = isRunAppGrantDenied(value);
    const mcpServers = Array.isArray(value.mcpServers)
      ? value.mcpServers
      : isPlainObject(value.mcpServers)
        ? Object.values(value.mcpServers)
        : [];
    out[appId] = {
      appId,
      appTitle: optionalString(value.appTitle || value.title),
      surface: optionalString(value.surface) || appId,
      status: denied ? "denied" : "granted",
      granted: !denied,
      denied,
      deniedReason: denied ? optionalString(value.reason || value.deniedReason || value.denied_reason || value.status) : null,
      deniedText: denied ? optionalString(value.deniedText || value.denied_text) || ACCESS_DENIED_TEXT : null,
      updatedAt: optionalString(value.updatedAt || value.updated_at),
      context: redactForPublicDiagnostics(isPlainObject(value.context) ? value.context : {}),
      scopes: uniqueStrings(Array.isArray(value.scopes) ? value.scopes : []),
      hasMcpServers: !denied && mcpServers.length > 0,
      mcpServerNames: mcpServers
        .map((server) => safeMcpServerName(server?.serverName || server?.server_name || server?.name))
        .filter(Boolean),
    };
  }
  return out;
}

function buildRunProfileAppCatalog({ appGrants, mcpServers, assistantActions }) {
  const grants = normalizeRunAppGrantsObject(appGrants);
  const allMcpServerNames = uniqueStrings((Array.isArray(mcpServers) ? mcpServers : [])
    .map((server) => server?.serverName)
    .filter(Boolean));
  const actionIds = Array.isArray(assistantActions?.actionIds) ? assistantActions.actionIds : [];
  return APP_ROUTING_CATALOG.map((entry) => {
    const grant = isPlainObject(grants[entry.appId]) ? grants[entry.appId] : null;
    const grantedByLegacyMcp = !grant && entry.mcpServerNames.some((name) => allMcpServerNames.includes(name));
    const denied = grant ? isRunAppGrantDenied(grant) : !grantedByLegacyMcp;
    const status = grant ? (denied ? "denied" : "granted") : grantedByLegacyMcp ? "granted" : "not-granted";
    return {
      schemaVersion: "ai-workspace.app-route.v1",
      appId: entry.appId,
      appTitle: entry.appTitle,
      surface: entry.surface,
      skillId: entry.skillId,
      status,
      granted: status === "granted",
      denied: status === "denied",
      notGranted: status === "not-granted",
      deniedText: status === "denied" ? entry.deniedText : null,
      actionIds: actionIds.filter((actionId) => actionIdPrefixesForEntry(entry).some((prefix) => String(actionId || "").startsWith(prefix))),
      mcpServerNames: status === "granted"
        ? uniqueStrings([
          ...entry.mcpServerNames,
          ...((Array.isArray(grant?.mcpServers) ? grant.mcpServers : []).map((server) => safeMcpServerName(server?.serverName || server?.server_name || server?.name))),
        ])
        : [],
      scopes: uniqueStrings(Array.isArray(grant?.scopes) ? grant.scopes : []),
    };
  });
}

function actionIdPrefixesForEntry(entry) {
  return uniqueStrings([
    ...(Array.isArray(entry.actionIdPrefixes) ? entry.actionIdPrefixes : []),
    `${entry.appId}.`,
    ...(Array.isArray(entry.appAliases) ? entry.appAliases.map((alias) => `${normalizeKey(alias)}.`) : []),
  ].filter(Boolean));
}

function summarizeRunAppAccess(appCatalog) {
  const apps = Array.isArray(appCatalog) ? appCatalog : [];
  return {
    schemaVersion: "ai-workspace.app-access.v1",
    grantedAppIds: apps.filter((app) => app?.granted === true).map((app) => app.appId).sort(),
    deniedAppIds: apps.filter((app) => app?.status === "denied" || app?.denied === true).map((app) => app.appId).sort(),
    notGrantedAppIds: apps.filter((app) => app?.status === "not-granted").map((app) => app.appId).sort(),
    availableSkillIds: apps.filter((app) => app?.granted === true).map((app) => app.skillId).filter(Boolean).sort(),
  };
}

function isRunAppGrantDenied(grant) {
  if (!isPlainObject(grant)) return false;
  const status = normalizeKey(grant.status || grant.state || grant.accessStatus || grant.access_status);
  return grant.enabled === false ||
    grant.allowed === false ||
    grant.granted === false ||
    grant.denied === true ||
    ["denied", "disabled", "blocked", "revoked", "not-granted", "not_granted", "forbidden"].includes(status);
}

function collectInstallerMcpServers(target, value, defaults = {}) {
  const items = Array.isArray(value)
    ? value
    : isPlainObject(value)
      ? Object.values(value)
      : [];
  for (const item of items) {
    if (!isPlainObject(item)) continue;
    target.push({ ...defaults, ...item });
  }
}

function sanitizeInstallerMcpServer(raw) {
  if (!isPlainObject(raw)) return null;
  const serverName = safeMcpServerName(raw.serverName || raw.server_name || raw.name);
  const url = optionalString(raw.url);
  if (!serverName || !url) return null;
  const httpHeaders = sanitizeInstallerMcpHeaders(raw.httpHeaders || raw.http_headers || raw.headers);
  return {
    appId: normalizeKey(raw.appId || raw.app_id) || "global",
    appTitle: optionalString(raw.appTitle || raw.app_title || raw.title),
    serverName,
    url,
    enabled: raw.enabled !== false,
    required: raw.required === true,
    startupTimeoutSec: sanitizeInteger(raw.startupTimeoutSec || raw.startup_timeout_sec, 20, 1, 600),
    toolTimeoutSec: sanitizeInteger(raw.toolTimeoutSec || raw.tool_timeout_sec, 60, 1, 3600),
    httpHeaders,
  };
}

function sanitizeInstallerMcpHeaders(value) {
  if (!isPlainObject(value)) return {};
  const headers = {};
  for (const [key, rawValue] of Object.entries(value)) {
    const headerName = optionalString(key);
    const headerValue = optionalString(rawValue);
    if (!headerName || !headerValue || headerName.length > 120 || headerValue.length > 4000) continue;
    headers[headerName] = headerValue;
  }
  return headers;
}

function runProfileHash(runProfile) {
  const publicProfile = redactRunProfile(runProfile);
  const stableProfile = {
    ...publicProfile,
    runId: undefined,
    createdAt: undefined,
    diagnostics: {
      ...(isPlainObject(publicProfile?.diagnostics) ? publicProfile.diagnostics : {}),
      profileHash: undefined,
    },
  };
  return createHash("sha256").update(JSON.stringify(stableProfile)).digest("hex").slice(0, 16);
}

function redactRunProfile(runProfile) {
  if (!isPlainObject(runProfile)) return null;
  return {
    ...runProfile,
    targetContexts: redactForPublicDiagnostics(runProfile.targetContexts),
    appGrants: redactForPublicDiagnostics(runProfile.appGrants),
    toolProfile: {
      ...(isPlainObject(runProfile.toolProfile) ? runProfile.toolProfile : {}),
      assistantActions: redactAssistantActions(runProfile.toolProfile?.assistantActions),
      mcpServers: Array.isArray(runProfile.toolProfile?.mcpServers)
        ? runProfile.toolProfile.mcpServers.map(redactMcpServer)
        : [],
    },
  };
}

function redactAssistantActions(value) {
  if (!isPlainObject(value)) return value;
  return {
    ...value,
    ...(value.gatewayToken ? { gatewayToken: "<redacted>" } : {}),
  };
}

function redactMcpServer(server) {
  if (!isPlainObject(server)) return {};
  const headers = isPlainObject(server.httpHeaders) ? server.httpHeaders : {};
  return {
    ...server,
    httpHeaders: Object.fromEntries(Object.keys(headers).map((key) => [key, "<redacted>"])),
    headers: undefined,
  };
}

function redactForPublicDiagnostics(value, depth = 0) {
  if (depth > 6) return "[max-depth]";
  if (Array.isArray(value)) return value.map((item) => redactForPublicDiagnostics(item, depth + 1));
  if (!isPlainObject(value)) return value;
  const out = {};
  for (const [key, item] of Object.entries(value)) {
    const normalizedKey = normalizeKey(key);
    if (
      normalizedKey.includes("token")
      || normalizedKey.includes("secret")
      || normalizedKey.includes("password")
      || normalizedKey === "authorization"
      || normalizedKey === "cookie"
      || normalizedKey === "setcookie"
    ) {
      out[key] = "<redacted>";
    } else {
      out[key] = redactForPublicDiagnostics(item, depth + 1);
    }
  }
  return out;
}

function safeMcpServerName(value) {
  const text = optionalString(value);
  if (!text) return "";
  return text.replace(/[^A-Za-z0-9_-]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 80);
}

function sanitizeInteger(value, fallback, min, max) {
  const number = Number(value || fallback);
  if (!Number.isFinite(number)) return fallback;
  return Math.min(Math.max(Math.trunc(number), min), max);
}

function uniqueStrings(values) {
  return Array.from(new Set((Array.isArray(values) ? values : []).map(optionalString).filter(Boolean)));
}

function normalizeKey(value) {
  return optionalString(value).toLowerCase().replace(/[^a-z0-9_-]+/g, "_").replace(/^_+|_+$/g, "");
}

function optionalString(value) {
  if (value === null || value === undefined) return "";
  const text = String(value).trim();
  return text.length ? text : "";
}

function isPlainObject(value) {
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
