import { describe, test } from 'node:test';
import assert from 'node:assert/strict';
import fc from 'fast-check';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { CondensedGraph } from '../../src/core/CondensedGraph.js';
import { ShardedSnapshotBuilder } from '../../src/core/shards/ShardedSnapshotBuilder.js';
import { ShardedSnapshot } from '../../src/core/shards/ShardedSnapshot.js';
import { FileShardStorage } from '../../src/core/shards/FileShardStorage.js';
import { DeltaShardBinary } from '../../src/core/shards/DeltaShardBinary.js';
import { WaveletShardBinary } from '../../src/core/shards/WaveletShardBinary.js';

function buildSnapshot(bucketSize = 4) {
  const graph = new CondensedGraph();
  const nodes = [];
  for (let i = 0; i < 6; i++) {
    nodes.push(graph._ensureNode(`node:${i}`));
  }
  graph.addEdge(nodes[0], 'owner', nodes[1]);
  graph.addEdge(nodes[0], 'owner', nodes[2]);
  graph.addEdge(nodes[3], 'owner', nodes[4]);
  graph.addEdge(nodes[5], 'owner', nodes[0]);

  graph.finalizePerfectHash();
  graph.finalizeWaveletAdjacency({ dropAdjacencyList: true });

  const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'sharded-delta-fc-'));
  const builder = new ShardedSnapshotBuilder({ bucketSize, includeDirections: ['out', 'in'] });
  const manifest = builder.build(graph, dir);
  const storage = new FileShardStorage(dir);
  const snapshot = new ShardedSnapshot(manifest, storage, { cacheLimit: 8 });
  snapshot.initializeSync();
  return { snapshot, manifest, dir, nodes };
}

function buildBaseEdges(nodes) {
  const edges = new Map();
  const add = (srcIdx, dstIdx) => {
    const srcId = nodes[srcIdx];
    const dstId = nodes[dstIdx];
    const set = edges.get(srcId) || new Set();
    set.add(dstId);
    edges.set(srcId, set);
  };
  add(0, 1);
  add(0, 2);
  add(3, 4);
  add(5, 0);
  return edges;
}

function cloneEdges(edges) {
  const next = new Map();
  for (const [src, set] of edges.entries()) {
    next.set(src, new Set(set));
  }
  return next;
}

function applyOps(edges, ops, mode = 'override') {
  const next = cloneEdges(edges);
  if (mode !== 'union') {
    for (const op of ops) {
      if (op.op !== 'remove') continue;
      const set = next.get(op.srcId) || new Set();
      set.delete(op.dstId);
      if (set.size > 0) next.set(op.srcId, set);
    }
  }
  for (const op of ops) {
    if (op.op !== 'add') continue;
    const set = next.get(op.srcId) || new Set();
    set.add(op.dstId);
    if (set.size > 0) next.set(op.srcId, set);
  }
  return next;
}

function hasEdge(edges, srcId, dstId) {
  const set = edges.get(srcId);
  return set ? set.has(dstId) : false;
}

function writeDeltaLayer(snapshot, dir, entries, mode = 'override') {
  fs.mkdirSync(dir, { recursive: true });
  const merged = new Map();
  for (const entry of entries) {
    const shardMeta = snapshot._selectShardMeta(entry.relationId, entry.direction, entry.srcId);
    assert.ok(shardMeta, 'Missing shard meta for delta entry');
    const localSource = snapshot._localSource(entry.srcId, shardMeta);
    const key = shardMeta.cacheKey;
    let bucket = merged.get(key);
    if (!bucket) {
      bucket = { shardMeta, additions: [], removals: [] };
      merged.set(key, bucket);
    }
    for (const add of entry.additions) {
      bucket.additions.push({
        srcLocal: localSource,
        otherId: add.dstId,
        possBits: add.possBits,
        relBits: add.relBits
      });
    }
    for (const rem of entry.removals) {
      bucket.removals.push({
        srcLocal: localSource,
        otherId: rem.dstId
      });
    }
  }

  const shards = [];
  for (const bucket of merged.values()) {
    const shardMeta = bucket.shardMeta;
    const buffer = DeltaShardBinary.serialize({
      relationId: shardMeta.relationId,
      direction: shardMeta.direction,
      rangeStart: shardMeta.rangeStart,
      rangeEnd: shardMeta.rangeEnd,
      nodeCount: snapshot.nodeCount,
      additions: bucket.additions,
      removals: bucket.removals
    });
    const shardKey = `delta-${shardMeta.key}`;
    fs.writeFileSync(path.join(dir, shardKey), new Uint8Array(buffer));
    shards.push({
      key: shardKey,
      relationId: shardMeta.relationId,
      direction: shardMeta.direction,
      rangeStart: shardMeta.rangeStart,
      rangeEnd: shardMeta.rangeEnd,
      cacheKey: shardMeta.cacheKey
    });
  }

  return { shards, storage: new FileShardStorage(dir), mode };
}

function compactLayer(snapshot, layer, manifest, outputDir, baseDir) {
  const sameDir = path.resolve(outputDir) === path.resolve(baseDir);
  if (!sameDir) {
    fs.rmSync(outputDir, { recursive: true, force: true });
  }
  fs.mkdirSync(outputDir, { recursive: true });

  if (!sameDir) {
    if (manifest.nodeTableKey) {
      const source = path.join(baseDir, manifest.nodeTableKey);
      const dest = path.join(outputDir, manifest.nodeTableKey);
      if (fs.existsSync(source)) fs.copyFileSync(source, dest);
    }
    if (manifest.componentKey) {
      const source = path.join(baseDir, manifest.componentKey);
      const dest = path.join(outputDir, manifest.componentKey);
      if (fs.existsSync(source)) fs.copyFileSync(source, dest);
    }
  }

  if (!sameDir) {
    for (const shard of manifest.shards || []) {
      fs.copyFileSync(path.join(baseDir, shard.key), path.join(outputDir, shard.key));
    }
  }

  for (const shardMeta of layer.shards) {
    const base = snapshot._cacheIndex.get(shardMeta.cacheKey);
    if (!base) continue;
    const shard = snapshot._loadShardSync(base.relationId, base.direction, base.rangeStart);
    if (!shard) continue;

    const deltaBuffer = layer.storage.getSync(shardMeta.key);
    if (!deltaBuffer) continue;
    const deltaShard = DeltaShardBinary.deserialize(deltaBuffer);
    const rangeSize = shard.rangeEnd - shard.rangeStart;
    const sources = new Array(rangeSize);
    for (let localSource = 0; localSource < rangeSize; localSource++) {
      const range = snapshot._rangeForSource(shard, localSource);
      const list = [];
      if (range) {
        for (let pos = range.start; pos < range.end; pos++) {
          list.push({ otherId: shard.dstIds[pos], possBits: shard.possBits[pos], relBits: shard.relBits[pos] });
        }
      }
      sources[localSource] = list;
    }

    for (const removal of deltaShard.removals) {
      const list = sources[removal.srcLocal];
      if (!list) continue;
      let idx = list.findIndex((item) => item.otherId === removal.otherId);
      while (idx !== -1) {
        list.splice(idx, 1);
        idx = list.findIndex((item) => item.otherId === removal.otherId);
      }
    }

    for (const addition of deltaShard.additions) {
      const list = sources[addition.srcLocal] || (sources[addition.srcLocal] = []);
      const idx = list.findIndex((item) => item.otherId === addition.otherId);
      if (idx === -1) {
        list.push({ otherId: addition.otherId, possBits: addition.possBits, relBits: addition.relBits });
      } else {
        list[idx] = { otherId: addition.otherId, possBits: addition.possBits, relBits: addition.relBits };
      }
    }

    const buffer = WaveletShardBinary.serialize({
      relationId: shard.relationId,
      direction: shard.direction,
      rangeStart: shard.rangeStart,
      rangeEnd: shard.rangeEnd,
      nodeCount: snapshot.nodeCount,
      sources
    });
    fs.writeFileSync(path.join(outputDir, base.key), new Uint8Array(buffer));
  }
}

describe.skip('Fast-check: delta layer equivalence', () => {
  test('multi-layer override equals compacted base', () => {
    fc.assert(
      fc.property(
        fc.array(
          fc.record({
            src: fc.integer({ min: 0, max: 5 }),
            dst: fc.integer({ min: 0, max: 5 }),
            op: fc.constantFrom('add', 'remove')
          }),
          { minLength: 1, maxLength: 20 }
        ),
        fc.array(
          fc.record({
            src: fc.integer({ min: 0, max: 5 }),
            dst: fc.integer({ min: 0, max: 5 }),
            op: fc.constantFrom('add', 'remove')
          }),
          { minLength: 1, maxLength: 20 }
        ),
        (layerA, layerB) => {
          const { snapshot, manifest, dir, nodes } = buildSnapshot(4);
          const relId = snapshot.relationIdToName.indexOf('owner');

          const toEntries = (ops) => ops.map((edge) => ({
            relationId: relId,
            direction: 'out',
            srcId: nodes[edge.src % nodes.length],
            additions: edge.op === 'add' ? [{ dstId: nodes[edge.dst % nodes.length], possBits: 65535, relBits: 65535 }] : [],
            removals: edge.op === 'remove' ? [{ dstId: nodes[edge.dst % nodes.length] }] : []
          }));

          const layer1 = writeDeltaLayer(snapshot, path.join(dir, 'l1'), toEntries(layerA), 'override');
          const layer2 = writeDeltaLayer(snapshot, path.join(dir, 'l2'), toEntries(layerB), 'override');
          snapshot.setDeltaLayers([layer1, layer2]);

          const base = buildBaseEdges(nodes);
          const layerAOps = layerA.map((edge) => ({
            srcId: nodes[edge.src % nodes.length],
            dstId: nodes[edge.dst % nodes.length],
            op: edge.op
          }));
          const layerBOps = layerB.map((edge) => ({
            srcId: nodes[edge.src % nodes.length],
            dstId: nodes[edge.dst % nodes.length],
            op: edge.op
          }));
          const expected = applyOps(applyOps(base, layerAOps, 'override'), layerBOps, 'override');

          const compactDir = path.join(dir, 'compact');
          compactLayer(snapshot, layer1, manifest, compactDir, dir);
          const compactSnapshot = new ShardedSnapshot(manifest, new FileShardStorage(compactDir), { cacheLimit: 8 });
          compactSnapshot.initializeSync();

          compactLayer(compactSnapshot, layer2, manifest, compactDir, compactDir);
          const compactSnapshot2 = new ShardedSnapshot(manifest, new FileShardStorage(compactDir), { cacheLimit: 8 });
          compactSnapshot2.initializeSync();

          for (const src of nodes) {
            for (const dst of nodes) {
              const overlayEdge = snapshot.findEdgeSync(src, relId, dst);
              const compactEdge = compactSnapshot2.findEdgeSync(src, relId, dst);
              const expectedEdge = hasEdge(expected, src, dst);
              assert.strictEqual(!!overlayEdge, !!compactEdge);
              assert.strictEqual(!!overlayEdge, expectedEdge);
            }
          }

          fs.rmSync(dir, { recursive: true, force: true });
        }
      ),
      { numRuns: 30 }
    );
  });
});
