import assert from "node:assert/strict";
import { Pool } from "pg";
import { validateDataProductHistory, validateDataProductPatch, validateDataProductSnapshot } from "@nodedc/external-provider-contract/data-plane";
import {
  loadDataProductDefinition,
  persistDataProductDefinition,
  persistDataProductPublish,
  readDataProductHistory,
  readDataProductSnapshot,
  readPatchEvents,
} from "../src/data-product-delivery.mjs";
import { normalizeDataProductDefinition } from "../src/data-product-policy.mjs";
import { migrate } from "../src/schema.mjs";

const databaseUrl = process.env.EXTERNAL_DATA_PLANE_TEST_DATABASE_URL;
if (!databaseUrl) throw new Error("EXTERNAL_DATA_PLANE_TEST_DATABASE_URL_required");
const pool = new Pool({ connectionString: databaseUrl, max: 4 });

try {
  await migrate(pool);
  await pool.query(`truncate table
    external_data_plane_patch_outbox,
    external_data_plane_history,
    external_data_plane_current,
    external_data_plane_facts,
    external_data_plane_raw_envelopes,
    external_data_plane_batches,
    external_data_plane_delivery_state,
    external_data_plane_reader_bindings,
    external_data_plane_writer_bindings,
    external_data_plane_products
    restart identity cascade`);

  const definition = normalizeDataProductDefinition({
    id: "test.positions.current.v1",
    version: "1.0.0",
    ontologyRevision: "ontology.test.positions.v1",
    deliveryMode: "snapshot+patch",
    semanticTypes: ["map.moving_object"],
    fields: ["source_id", "observed_at", "geometry", "status"],
    fieldContracts: {
      source_id: { type: "string", required: true },
      observed_at: { type: "string", required: true },
      geometry: { type: "point", required: true },
      status: { type: "string", required: true },
    },
    history: {
      mode: "sampled",
      intervalMs: 60_000,
      strategy: "latest-per-entity-per-bucket",
      retentionDays: 30,
    },
  });
  await persistDataProductDefinition(pool, definition);
  const storedDefinition = await loadDataProductDefinition(pool, definition.id);
  assert.deepEqual(storedDefinition.fieldContracts, definition.fieldContracts);
  const binding = {
    id: "reader-binding-test",
    bindingKey: "managed-reader-binding-test",
    tenantId: "tenant-test",
    connectionId: "consumer-connection-test",
    sourceConnectionId: "connection-test",
    providerId: "provider-test",
    allowedDataProductIds: [definition.id],
    active: true,
    expiresAt: "2027-01-01T00:00:00.000Z",
  };

  const firstBatch = batch("run-01", "2026-07-15T10:00:05.000Z", [
    fact("unit-01", 37.61, 55.75, "online"),
    fact("unit-02", 37.62, 55.76, "online"),
  ]);
  const first = await persistDataProductPublish(pool, firstBatch, storedDefinition);
  assert.equal(first.idempotent, false);
  assert.equal(first.currentUpdatedCount, 2);
  assert.equal(first.historyInsertedCount, 2);
  assert.equal(first.cursor, "1");

  const duplicate = await persistDataProductPublish(pool, firstBatch, storedDefinition);
  assert.equal(duplicate.idempotent, true);
  assert.equal(duplicate.cursor, "1");

  await assert.rejects(
    persistDataProductPublish(pool, {
      ...firstBatch,
      facts: [fact("unit-01", 99.9, 55.75, "changed")],
    }, storedDefinition),
    (error) => error?.status === 409 && error?.code === "idempotency_key_reused",
  );

  const concurrentBatch = batch("run-concurrent", "2026-07-15T10:00:08.000Z", [
    fact("unit-03", 37.63, 55.77, "online", "2026-07-15T10:00:08.000Z"),
  ]);
  const concurrent = await Promise.all([
    persistDataProductPublish(pool, concurrentBatch, storedDefinition),
    persistDataProductPublish(pool, concurrentBatch, storedDefinition),
  ]);
  assert.deepEqual(concurrent.map((value) => value.idempotent).sort(), [false, true]);

  const second = await persistDataProductPublish(pool, batch(
    "run-02",
    "2026-07-15T10:00:15.000Z",
    [fact("unit-01", 37.611, 55.751, "moving", "2026-07-15T10:00:15.000Z")],
  ), storedDefinition);
  assert.equal(second.currentUpdatedCount, 1);
  assert.equal(second.cursor, "3");

  const latest = await persistDataProductPublish(pool, batch(
    "run-latest",
    "2026-07-15T10:02:05.000Z",
    [fact("unit-01", 37.7, 55.8, "latest", "2026-07-15T10:02:00.000Z")],
  ), storedDefinition);
  assert.equal(latest.currentUpdatedCount, 1);
  const late = await persistDataProductPublish(pool, batch(
    "run-late",
    "2026-07-15T10:03:00.000Z",
    [fact("unit-01", 37.65, 55.78, "late", "2026-07-15T10:01:30.000Z")],
  ), storedDefinition);
  assert.equal(late.currentUpdatedCount, 0);
  assert.equal(late.historyInsertedCount, 1);
  assert.equal(late.patchOperationCount, 0);

  await assert.rejects(
    persistDataProductPublish(pool, batch("run-bad-type", "2026-07-15T10:03:10.000Z", [{
      ...fact("unit-04", 37.6, 55.7, "online"),
      semanticType: "map.forbidden",
    }]), storedDefinition),
    (error) => error?.status === 422 && error?.code === "data_product_semantic_type_forbidden",
  );
  await assert.rejects(
    persistDataProductPublish(pool, batch("run-bad-field", "2026-07-15T10:03:11.000Z", [{
      ...fact("unit-04", 37.6, 55.7, "online"),
      attributes: { undeclared: true },
    }]), storedDefinition),
    (error) => error?.status === 422 && error?.code === "data_product_field_forbidden",
  );
  await assert.rejects(
    persistDataProductPublish(pool, batch("run-duplicate-fact", "2026-07-15T10:03:12.000Z", [
      fact("unit-04", 37.6, 55.7, "online"),
      fact("unit-04", 37.61, 55.71, "moving"),
    ]), storedDefinition),
    (error) => error?.status === 422 && error?.code === "data_product_duplicate_entity_key",
  );

  const snapshot = await readDataProductSnapshot(pool, binding, storedDefinition);
  assert.equal(validateDataProductSnapshot(snapshot).ok, true);
  assert.equal(snapshot.cursor, "4");
  assert.equal(snapshot.facts.length, 3);
  assert.equal(snapshot.facts.find((value) => value.sourceId === "unit-01").attributes.status, "latest");

  const patches = await readPatchEvents(pool, binding, storedDefinition, 0n);
  assert.equal(patches.length, 4);
  assert.equal(patches.every((patch) => validateDataProductPatch(patch).ok), true);
  assert.deepEqual(patches.map((patch) => patch.cursor), ["1", "2", "3", "4"]);

  await assert.rejects(
    readPatchEvents(pool, binding, storedDefinition, 999n),
    (error) => error?.status === 409 && error?.code === "resync_required",
  );

  const history = await pool.query("select source_id, observed_at from external_data_plane_history order by source_id");
  assert.equal(history.rowCount, 5);
  const unitOneHistory = history.rows.filter((row) => row.source_id === "unit-01");
  assert.equal(unitOneHistory.some((row) => new Date(row.observed_at).toISOString() === "2026-07-15T10:01:30.000Z"), true);
  assert.equal(unitOneHistory.some((row) => new Date(row.observed_at).toISOString() === "2026-07-15T10:02:00.000Z"), true);

  const historyPageOne = await readDataProductHistory(pool, binding, storedDefinition, {
    from: "2026-07-15T10:00:00.000Z",
    to: "2026-07-15T10:04:00.000Z",
    resolutionMs: 60_000,
    sourceIds: ["unit-01"],
    limit: 2,
  });
  assert.equal(validateDataProductHistory(historyPageOne).ok, true);
  assert.equal(historyPageOne.facts.length, 2);
  assert.equal(typeof historyPageOne.nextCursor, "string");
  const historyPageTwo = await readDataProductHistory(pool, binding, storedDefinition, {
    from: "2026-07-15T10:00:00.000Z",
    to: "2026-07-15T10:04:00.000Z",
    resolutionMs: 60_000,
    sourceIds: ["unit-01"],
    limit: 2,
    cursor: historyPageOne.nextCursor,
  });
  assert.equal(validateDataProductHistory(historyPageTwo).ok, true);
  assert.equal(historyPageTwo.facts.length, 1);
  assert.equal(historyPageTwo.nextCursor, undefined);
  assert.deepEqual(
    [...historyPageOne.facts, ...historyPageTwo.facts].map((value) => value.bucketStart),
    [
      "2026-07-15T10:00:00.000Z",
      "2026-07-15T10:01:00.000Z",
      "2026-07-15T10:02:00.000Z",
    ],
  );
  await assert.rejects(
    readDataProductHistory(pool, binding, storedDefinition, {
      from: "2026-07-15T10:00:00.000Z",
      to: "2026-07-15T10:04:00.000Z",
      resolutionMs: 60_000,
      sourceIds: ["unit-02"],
      limit: 2,
      cursor: historyPageOne.nextCursor,
    }),
    (error) => error?.status === 400 && error?.code === "data_product_history_cursor_invalid",
  );

  const zoneDefinition = normalizeDataProductDefinition({
    id: "test.zones.current.v1",
    version: "1.0.0",
    ontologyRevision: "ontology.map.zone.v1",
    deliveryMode: "snapshot+patch",
    semanticTypes: ["map.zone"],
    fields: ["display_name", "geometry", "geometry_kind", "source_kind", "source_revision"],
    fieldContracts: {
      display_name: { type: "string", required: true },
      geometry: { type: "geometry", required: true },
      geometry_kind: { type: "string", required: true, enum: ["polygon"] },
      source_kind: { type: "string", required: true, enum: ["live_api", "versioned_snapshot"] },
      source_revision: { type: "string", required: true },
    },
    history: { mode: "none", retentionDays: 1 },
  });
  await persistDataProductDefinition(pool, zoneDefinition);
  const storedZoneDefinition = await loadDataProductDefinition(pool, zoneDefinition.id);
  const zoneBinding = { ...binding, allowedDataProductIds: [zoneDefinition.id] };
  const firstZoneGenerationAt = "2026-07-15T10:10:00.000Z";
  const firstZoneGeneration = await persistDataProductPublish(
    pool,
    zoneBatch("zones-01", firstZoneGenerationAt, [zoneFact("zone-01", firstZoneGenerationAt), zoneFact("zone-02", firstZoneGenerationAt)]),
    storedZoneDefinition,
  );
  assert.equal(firstZoneGeneration.currentUpdatedCount, 2);
  assert.equal(firstZoneGeneration.currentRemovedCount, 0);
  const replayedFirstZoneGeneration = await persistDataProductPublish(
    pool,
    zoneBatch("zones-01-stable-replay-key", firstZoneGenerationAt, [
      zoneFact("zone-01", firstZoneGenerationAt),
      zoneFact("zone-02", firstZoneGenerationAt),
    ]),
    storedZoneDefinition,
  );
  assert.equal(replayedFirstZoneGeneration.idempotent, true);
  assert.equal(replayedFirstZoneGeneration.publishedFactCount, 2);
  assert.equal(replayedFirstZoneGeneration.currentUpdatedCount, 0);
  assert.equal(replayedFirstZoneGeneration.currentRemovedCount, 0);
  assert.equal(replayedFirstZoneGeneration.patchOperationCount, 0);
  assert.equal(replayedFirstZoneGeneration.cursor, firstZoneGeneration.cursor);
  await assert.rejects(
    persistDataProductPublish(pool, zoneBatch("zones-01-conflicting-replay", firstZoneGenerationAt, [
      zoneFact("zone-01", firstZoneGenerationAt),
    ]), storedZoneDefinition),
    (error) => error?.status === 409 && error?.code === "data_product_generation_not_newer",
  );
  const secondZoneGenerationAt = "2026-07-15T10:11:00.000Z";
  const secondZoneGeneration = await persistDataProductPublish(
    pool,
    zoneBatch("zones-02", secondZoneGenerationAt, [zoneFact("zone-01", secondZoneGenerationAt)]),
    storedZoneDefinition,
  );
  assert.equal(secondZoneGeneration.currentUpdatedCount, 1);
  assert.equal(secondZoneGeneration.currentRemovedCount, 1);
  assert.equal(secondZoneGeneration.patchOperationCount, 2);
  const zoneSnapshot = await readDataProductSnapshot(pool, zoneBinding, storedZoneDefinition);
  assert.equal(validateDataProductSnapshot(zoneSnapshot).ok, true);
  assert.deepEqual(zoneSnapshot.facts.map((value) => value.sourceId), ["zone-01"]);
  assert.equal(zoneSnapshot.facts[0].geometry.type, "Polygon");
  const zonePatches = await readPatchEvents(pool, zoneBinding, storedZoneDefinition, 0n);
  assert.equal(zonePatches.every((value) => validateDataProductPatch(value).ok), true);
  assert.equal(zonePatches.flatMap((value) => value.operations).some((value) => value.op === "remove" && value.sourceId === "zone-02"), true);
  await assert.rejects(
    persistDataProductPublish(pool, zoneBatch("zones-stale", firstZoneGenerationAt, [zoneFact("zone-01", firstZoneGenerationAt)]), storedZoneDefinition),
    (error) => error?.status === 409 && error?.code === "data_product_generation_not_newer",
  );

  console.log("external-data-plane delivery integration: ok");
} finally {
  await pool.end();
}

function batch(runId, receivedAt, facts) {
  return {
    schemaVersion: "nodedc.external-provider-contract/v1",
    source: { tenantId: "tenant-test", connectionId: "connection-test", providerId: "provider-test" },
    contract: {
      dataProductId: "test.positions.current.v1",
      ontologyRevision: "ontology.test.positions.v1",
      version: "1.0.0",
    },
    batch: { runId, sequence: 0, idempotencyKey: `${runId}.chunk-0`, receivedAt },
    facts,
  };
}

function fact(sourceId, longitude, latitude, status, observedAt = "2026-07-15T10:00:00.000Z") {
  return {
    sourceId,
    semanticType: "map.moving_object",
    observedAt,
    attributes: { status },
    geometry: { type: "Point", coordinates: [longitude, latitude] },
  };
}

function zoneBatch(runId, generationAt, facts) {
  return {
    schemaVersion: "nodedc.external-provider-contract/v1",
    source: { tenantId: "tenant-test", connectionId: "connection-test", providerId: "provider-test" },
    contract: { dataProductId: "test.zones.current.v1", ontologyRevision: "ontology.map.zone.v1", version: "1.0.0" },
    batch: {
      runId,
      sequence: 0,
      idempotencyKey: `${runId}.chunk-0`,
      receivedAt: generationAt,
      mode: "replace",
      generationAt,
    },
    facts,
  };
}

function zoneFact(sourceId, observedAt) {
  return {
    sourceId,
    semanticType: "map.zone",
    observedAt,
    attributes: {
      display_name: sourceId,
      geometry_kind: "polygon",
      source_kind: "live_api",
      source_revision: "gelios-rest-v1",
    },
    geometry: {
      type: "Polygon",
      coordinates: [[[37.60, 55.74], [37.62, 55.74], [37.62, 55.76], [37.60, 55.74]]],
    },
  };
}
