import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import {
  L2_EXECUTION_PLAN_COMPILER_VERSION,
  L2_EXECUTION_PLAN_SUPPORTED_COMPILER_VERSIONS,
  L2_EXECUTION_PLAN_SCHEMA_VERSION,
  L2_GRAPH_BLUEPRINT_SCHEMA_VERSION,
  L2_MAPPING_RUNTIME_SCHEMA_VERSION,
  L2_MATERIALIZATION_RECEIPT_SCHEMA_VERSION,
  attestL2ExecutionPlanMaterialization,
  compileL2ExecutionPlan,
  compileL2GraphBlueprint,
  instantiateL2Connection,
  validateL2ExecutionPlan,
} from "../src/index.mjs";
import {
  geliosProviderPackageV11,
  geliosTelemetryFieldRegistryV4,
} from "../providers/gelios/v11/index.mjs";

const positionsConnection = instantiateL2Connection(geliosProviderPackageV11, {
  tenantId: "tenant-compiler-fixture",
  connectionId: "gelios-compiler-fixture",
  collectionProfileId: "gelios.positions.current.realtime.v7",
  providerCredentialRef: "ndc-credref:provider-compiler-fixture-0001",
});
const positionsPlan = compileL2ExecutionPlan(
  geliosProviderPackageV11,
  positionsConnection,
  { telemetryFieldRegistry: geliosTelemetryFieldRegistryV4 },
);

assert.equal(positionsPlan.schemaVersion, L2_EXECUTION_PLAN_SCHEMA_VERSION);
assert.equal(L2_EXECUTION_PLAN_COMPILER_VERSION, "1.5.0");
assert.deepEqual(L2_EXECUTION_PLAN_SUPPORTED_COMPILER_VERSIONS, ["1.1.0", "1.2.0", "1.3.0", "1.4.0", "1.5.0"]);
assert.equal(positionsPlan.compilerVersion, "1.5.0");
assert.deepEqual(validateL2ExecutionPlan(positionsPlan), { ok: true, errors: [] });
assert.match(positionsPlan.executionPlanDigest, /^sha256:[a-f0-9]{64}$/);
assert.match(positionsPlan.artifacts.packageDigest, /^sha256:[a-f0-9]{64}$/);
assert.match(positionsPlan.artifacts.mappingContractDigest, /^sha256:[a-f0-9]{64}$/);
assert.match(positionsPlan.artifacts.telemetryRegistryDigest, /^sha256:[a-f0-9]{64}$/);
assert.equal(positionsPlan.graphBlueprint.schemaVersion, L2_GRAPH_BLUEPRINT_SCHEMA_VERSION);
assert.equal(
  positionsPlan.graphBlueprint.runtime.mappingRuntime.schemaVersion,
  L2_MAPPING_RUNTIME_SCHEMA_VERSION,
);
assert.match(positionsPlan.graphBlueprint.blueprintDigest, /^sha256:[a-f0-9]{64}$/);
assert.equal(compileL2GraphBlueprint(positionsPlan), positionsPlan.graphBlueprint);
assert.deepEqual(
  positionsPlan.graphBlueprint.nodes.map(({ id, stepId, runtimeKind, order }) => ({
    id,
    stepId,
    runtimeKind,
    order,
  })),
  [
    { id: "collection.trigger.manual", stepId: "collection.trigger", runtimeKind: "ndc.manual-trigger", order: 0 },
    { id: "collection.trigger.interval", stepId: "collection.trigger", runtimeKind: "ndc.interval-trigger", order: 1 },
    { id: "provider.fetch-monitoring-config", stepId: "provider.fetch-monitoring-config", runtimeKind: "ndc.provider-request", order: 2 },
    { id: "provider.fetch-units", stepId: "provider.fetch-units", runtimeKind: "ndc.provider-request", order: 3 },
    { id: "provider.extract-units", stepId: "provider.extract-units", runtimeKind: "ndc.extract-items", order: 4 },
    { id: "ontology.map", stepId: "ontology.map", runtimeKind: "ndc.semantic-mapping", order: 5 },
    { id: "data-product.publish", stepId: "data-product.publish", runtimeKind: "ndc.data-product-publish", order: 6 },
  ],
);
assert.deepEqual(
  positionsPlan.graphBlueprint.edges.map(({ from, to }) => ({ from, to })),
  [
    { from: "collection.trigger.manual", to: "provider.fetch-monitoring-config" },
    { from: "collection.trigger.interval", to: "provider.fetch-monitoring-config" },
    { from: "provider.fetch-monitoring-config", to: "provider.fetch-units" },
    { from: "provider.fetch-units", to: "provider.extract-units" },
    { from: "provider.extract-units", to: "ontology.map" },
    { from: "ontology.map", to: "data-product.publish" },
  ],
);
assert.deepEqual(positionsPlan.graphBlueprint.runtime.mappingRuntime.derivationKinds, [
  "bounded_readings",
  "ordered_rules",
]);
assert.deepEqual(positionsPlan.graphBlueprint.runtime.retry, {
  maxAttempts: 4,
  backoff: "fixed_delay",
  delayMs: 2000,
});
assert.equal(
  JSON.stringify(positionsPlan.graphBlueprint).includes("ndc-credref:"),
  false,
);
assert.equal(
  positionsPlan.graphBlueprint.nodes.every((node) => !node.runtimeKind.includes("gelios")),
  true,
);
assert.deepEqual(positionsPlan.steps.map((step) => step.kind), [
  "collection_trigger",
  "provider_request",
  "provider_request",
  "extract_items",
  "semantic_mapping",
  "data_product_publish",
]);
const unitsRequest = positionsPlan.steps.find(
  (step) => step.kind === "provider_request" && step.config.capabilityId === "gelios.units.current.read",
);
assert.deepEqual(unitsRequest.config.request.query, {
  incltrip: "true",
  inclcntrs: "true",
  inclsnsrs: "true",
  incllsv: "true",
});
const semanticMapping = positionsPlan.steps.find((step) => step.kind === "semantic_mapping");
assert.deepEqual(
  semanticMapping.config.telemetryProjection.entries.map((entry) => entry.reading.id),
  [
    "sensor.param.in_0",
    "sensor.param.in_1",
    "sensor.param.in0",
    "sensor.param.in1",
  ],
);
assert.equal(Object.isFrozen(positionsPlan), true);
assert.equal(Object.isFrozen(positionsPlan.steps), true);
assert.equal(Object.isFrozen(positionsPlan.graphBlueprint), true);

const clonedPlan = compileL2ExecutionPlan(
  structuredClone(geliosProviderPackageV11),
  structuredClone(positionsConnection),
  { telemetryFieldRegistry: structuredClone(geliosTelemetryFieldRegistryV4) },
);
assert.equal(clonedPlan.executionPlanDigest, positionsPlan.executionPlanDigest);

const legacyCompilerPlan = structuredClone(positionsPlan);
legacyCompilerPlan.compilerVersion = "1.1.0";
legacyCompilerPlan.executionPlanDigest = digestWithout(
  legacyCompilerPlan,
  "executionPlanDigest",
);
assert.deepEqual(validateL2ExecutionPlan(legacyCompilerPlan), { ok: true, errors: [] });

const unsupportedCompilerPlan = structuredClone(positionsPlan);
unsupportedCompilerPlan.compilerVersion = "2.0.0";
unsupportedCompilerPlan.executionPlanDigest = digestWithout(
  unsupportedCompilerPlan,
  "executionPlanDigest",
);
assert.equal(
  validateL2ExecutionPlan(unsupportedCompilerPlan).errors.includes(
    "executionPlan.compilerVersion_mismatch",
  ),
  true,
);

assert.throws(
  () => compileL2ExecutionPlan(geliosProviderPackageV11, positionsConnection),
  /telemetry_registry_required/,
);

const profileConnection = instantiateL2Connection(geliosProviderPackageV11, {
  tenantId: "tenant-compiler-fixture",
  connectionId: "gelios-profile-compiler-fixture",
  collectionProfileId: "gelios.units.profile.cold.v1",
  providerCredentialRef: "ndc-credref:provider-compiler-fixture-0001",
});
const profilePlan = compileL2ExecutionPlan(geliosProviderPackageV11, profileConnection);
assert.deepEqual(validateL2ExecutionPlan(profilePlan), { ok: true, errors: [] });
assert.equal(profilePlan.artifacts.telemetryRegistryDigest, undefined);
assert.notEqual(profilePlan.executionPlanDigest, positionsPlan.executionPlanDigest);
assert.deepEqual(profilePlan.graphBlueprint.runtime.mappingRuntime.derivationKinds, []);

for (const collectionProfile of geliosProviderPackageV11.collectionProfiles) {
  const connection = instantiateL2Connection(geliosProviderPackageV11, {
    tenantId: "tenant-all-profiles-fixture",
    connectionId: `connection-${collectionProfile.id.replaceAll(".", "-")}`,
    collectionProfileId: collectionProfile.id,
    providerCredentialRef: "ndc-credref:provider-all-profiles-fixture",
  });
  const plan = compileL2ExecutionPlan(
    geliosProviderPackageV11,
    connection,
    collectionProfile.dataProductId === "fleet.positions.current.v5"
      ? { telemetryFieldRegistry: geliosTelemetryFieldRegistryV4 }
      : {},
  );
  assert.deepEqual(validateL2ExecutionPlan(plan), { ok: true, errors: [] });
  assert.equal(JSON.stringify(plan.graphBlueprint.runtime.mappingRuntime).includes("gelios"), false);
  const expectedEntrypoints = collectionProfile.mode === "realtime" ? 2 : 1;
  assert.equal(plan.graphBlueprint.nodes.length, plan.steps.length + expectedEntrypoints - 1);
  assert.equal(plan.graphBlueprint.edges.length, plan.steps.length + expectedEntrypoints - 2);
}

const secondConnection = instantiateL2Connection(geliosProviderPackageV11, {
  tenantId: "tenant-compiler-fixture",
  connectionId: "gelios-compiler-fixture-two",
  collectionProfileId: "gelios.positions.current.realtime.v7",
  providerCredentialRef: "ndc-credref:provider-compiler-fixture-0002",
});
const secondPlan = compileL2ExecutionPlan(
  geliosProviderPackageV11,
  secondConnection,
  { telemetryFieldRegistry: geliosTelemetryFieldRegistryV4 },
);
assert.notEqual(secondPlan.executionPlanDigest, positionsPlan.executionPlanDigest);

const receipt = attestL2ExecutionPlanMaterialization(positionsPlan, {
  graphRevision: "engine-revision-fixture-0001",
  graphDigest: "a".repeat(64),
  materializedStepIds: positionsPlan.steps.map((step) => step.id),
});
assert.equal(receipt.schemaVersion, L2_MATERIALIZATION_RECEIPT_SCHEMA_VERSION);
assert.equal(receipt.executionPlanDigest, positionsPlan.executionPlanDigest);
assert.equal(receipt.graphDigest, `sha256:${"a".repeat(64)}`);
assert.match(receipt.receiptDigest, /^sha256:[a-f0-9]{64}$/);
assert.equal(Object.isFrozen(receipt), true);

assert.throws(() => attestL2ExecutionPlanMaterialization(positionsPlan, {
  graphRevision: "engine-revision-fixture-0001",
  graphDigest: "a".repeat(64),
  materializedStepIds: positionsPlan.steps.slice(1).map((step) => step.id),
}), /materializedStepIds_must_exactly_match/);

const tamperedPlan = structuredClone(positionsPlan);
tamperedPlan.steps[0].config.mode = "history";
assert.equal(
  validateL2ExecutionPlan(tamperedPlan).errors.includes("executionPlan.executionPlanDigest_mismatch"),
  true,
);

const authorityEscalatedPlan = structuredClone(positionsPlan);
authorityEscalatedPlan.graphBlueprint.runtime.target = "provider_specific_runtime";
authorityEscalatedPlan.graphBlueprint.blueprintDigest = digestWithout(
  authorityEscalatedPlan.graphBlueprint,
  "blueprintDigest",
);
authorityEscalatedPlan.executionPlanDigest = digestWithout(
  authorityEscalatedPlan,
  "executionPlanDigest",
);
assert.equal(
  validateL2ExecutionPlan(authorityEscalatedPlan).errors.includes("executionPlan.graphBlueprint_mismatch"),
  true,
);

const serialized = JSON.stringify(positionsPlan);
assert.equal(serialized.includes("Bearer "), false);
assert.equal(serialized.includes("access_token"), false);
assert.equal(serialized.includes("refresh_token"), false);

function digestWithout(value, key) {
  const descriptor = structuredClone(value);
  delete descriptor[key];
  return `sha256:${createHash("sha256").update(JSON.stringify(stableValue(descriptor)), "utf8").digest("hex")}`;
}

function stableValue(value) {
  if (Array.isArray(value)) return value.map(stableValue);
  if (!value || typeof value !== "object") return value;
  return Object.fromEntries(
    Object.keys(value)
      .filter((key) => value[key] !== undefined)
      .sort()
      .map((key) => [key, stableValue(value[key])]),
  );
}
