import { describe, it, expect, afterEach, beforeAll, afterAll, beforeEach } from 'vitest';
import {
  DKGNode,
  ProtocolRouter,
  TypedEventBus,
  generateEd25519Keypair,
  PROTOCOL_PUBLISH,
  PROTOCOL_ACCESS,
  encodePublishRequest,
  decodePublishRequest,
  decodePublishAck,
} from '@origintrail-official/dkg-core';
import { OxigraphStore, type Quad } from '@origintrail-official/dkg-storage';
import { EVMChainAdapter, buildKnowledgeAssetUal } from '@origintrail-official/dkg-chain';
import { DKGPublisher } from '../src/dkg-publisher.js';
import { PublishHandler } from '../src/publish-handler.js';
import { AccessHandler } from '../src/access-handler.js';
import { AccessClient } from '../src/access-client.js';
import { createSubstrateClient, registerSubstrateHandler } from './_helpers/substrate.js';
import { DKGQueryEngine } from '@origintrail-official/dkg-query';
import { multiaddr } from '@multiformats/multiaddr';
import { ethers } from 'ethers';
import { computeStructuredKCRootV10 } from '../src/merkle.js';
import { parseSimpleNQuads } from '../src/publish-handler.js';
import { createEVMAdapter, getSharedContext, createProvider, takeSnapshot, revertSnapshot, createTestContextGraph, HARDHAT_KEYS } from '../../chain/test/evm-test-context.js';
import { mintTokens } from '../../chain/test/hardhat-harness.js';
import { wrapPublisherForTest } from './_helpers/seal.js';
import { makeTestKaAllocator } from './_helpers/ka-allocator.js';
import { hardhatACKProvider } from './_helpers/acks.js';

let CONTEXT_GRAPH = 'agent-skills';
let _kav10Address: string;
let _provider: ethers.JsonRpcProvider;
const _author = new ethers.Wallet(HARDHAT_KEYS.CORE_OP);

function makeTestPublisher(opts: ConstructorParameters<typeof DKGPublisher>[0]): DKGPublisher {
  // OT-RFC-43 Option-1: the real EVM adapter requires a packed reservedKaId
  // per mint, which DKGPublisher only allocates when a kaAllocator is wired.
  return wrapPublisherForTest(new DKGPublisher({ kaAllocator: makeTestKaAllocator(), ...opts }), {
    author: _author,
    ctx: { provider: _provider, kav10Address: _kav10Address },
    v10ACKProvider: hardhatACKProvider(_kav10Address),
  });
}
let GRAPH = `did:dkg:context-graph:${CONTEXT_GRAPH}`;
const ENTITY = 'did:dkg:agent:QmImageBot';
const publisherWallet = new ethers.Wallet(HARDHAT_KEYS.CORE_OP);
const TEST_PUBLISHER_ADDRESS = publisherWallet.address;

function q(s: string, p: string, o: string, g = GRAPH): Quad {
  return { subject: s, predicate: p, object: o, graph: g };
}

describe('End-to-end: Publish → Replicate → Query', () => {
  const nodes: DKGNode[] = [];
  let snapshotId: string;

  beforeAll(async () => {
    snapshotId = await takeSnapshot();
    const { hubAddress } = getSharedContext();
    const provider = createProvider();
    await mintTokens(provider, hubAddress, HARDHAT_KEYS.DEPLOYER, TEST_PUBLISHER_ADDRESS, ethers.parseEther('5000000'));
    // Public CG (accessPolicy 0): these tests publish plaintext to exercise
    // publish/replicate/query lifecycle, not curated/ciphertext semantics.
    const cgId = await createTestContextGraph(undefined, undefined, 0);
    CONTEXT_GRAPH = String(cgId);
    GRAPH = `did:dkg:context-graph:${CONTEXT_GRAPH}`;
    _provider = provider;
    const chain = createEVMAdapter(HARDHAT_KEYS.CORE_OP);
    _kav10Address = await chain.getKnowledgeAssetsLifecycleAddress();
  });

  afterAll(async () => {
    await revertSnapshot(snapshotId);
  });

  afterEach(async () => {
    for (const n of nodes) {
      await n.stop();
    }
    nodes.length = 0;
  });

  it('publishes on node A, replicates to node B, queries on B', async () => {
    // === Setup Node A (publisher) ===
    const nodeA = new DKGNode({
      listenAddresses: ['/ip4/127.0.0.1/tcp/0'],
      enableMdns: false,
    });
    const nodeB = new DKGNode({
      listenAddresses: ['/ip4/127.0.0.1/tcp/0'],
      enableMdns: false,
    });
    nodes.push(nodeA, nodeB);
    await nodeA.start();
    await nodeB.start();

    // Connect
    await nodeA.libp2p.dial(multiaddr(nodeB.multiaddrs[0]));
    await new Promise((r) => setTimeout(r, 500));

    // Stores
    const storeA = new OxigraphStore();
    const storeB = new OxigraphStore();
    const chainA = createEVMAdapter(HARDHAT_KEYS.CORE_OP);
    const busA = new TypedEventBus();
    const busB = new TypedEventBus();
    const keypairA = await generateEd25519Keypair();
    const keypairB = await generateEd25519Keypair();

    // Publisher on A
    const publisherA = makeTestPublisher({
      store: storeA,
      chain: chainA,
      eventBus: busA,
      keypair: keypairA,
      publisherPrivateKey: HARDHAT_KEYS.CORE_OP,
      publisherNodeIdentityId: BigInt(getSharedContext().coreProfileId),
    });

    const publishHandlerB = new PublishHandler(storeB, busB);
    const routerB = new ProtocolRouter(nodeB);
    routerB.register(PROTOCOL_PUBLISH, publishHandlerB.handler);

    // Router on A for sending
    const routerA = new ProtocolRouter(nodeA);

    // === Step 1: Publish on Node A ===
    const publishResult = await publisherA.publish({
      contextGraphId: CONTEXT_GRAPH,
      quads: [
        q(ENTITY, 'http://schema.org/name', '"ImageBot"'),
        q(ENTITY, 'http://schema.org/description', '"AI image analysis agent"'),
        q(ENTITY, 'http://ex.org/offers', `${ENTITY}/.well-known/genid/o1`),
        q(`${ENTITY}/.well-known/genid/o1`, 'http://ex.org/skill', '"ImageAnalysis"'),
      ],
    });

    expect(publishResult.merkleRoot).toHaveLength(32);
    expect(publishResult.kaManifest).toHaveLength(1);
    expect(publishResult.status).toBe('confirmed');

    // === Step 2: Replicate to Node B via protocol ===
    const nquads = [
      `<${ENTITY}> <http://schema.org/name> "ImageBot" <${GRAPH}> .`,
      `<${ENTITY}> <http://schema.org/description> "AI image analysis agent" <${GRAPH}> .`,
      `<${ENTITY}> <http://ex.org/offers> <${ENTITY}/.well-known/genid/o1> <${GRAPH}> .`,
      `<${ENTITY}/.well-known/genid/o1> <http://ex.org/skill> "ImageAnalysis" <${GRAPH}> .`,
    ].join('\n');

    const onChain = publishResult.onChainResult!;
    const kaContract =
      onChain.knowledgeAssetsContract ??
      (await chainA.getDKGKnowledgeAssetsAddress!());
    const publishRequest = encodePublishRequest({
      ual: buildKnowledgeAssetUal(chainA.chainId, kaContract, onChain.startKAId!),
      nquads: new TextEncoder().encode(nquads),
      contextGraphId: CONTEXT_GRAPH,
      kas: publishResult.kaManifest.map((m) => ({
        // OT-RFC-43 Option-1: kaIds are packed ~256-bit values — never narrow
        // through Number() (lossy float → idToProtoString emits exponential →
        // receiver BigInt() throws). Pass the bigint id straight through.
        tokenId: m.tokenId,
        rootEntity: m.rootEntity,
        privateMerkleRoot: m.privateMerkleRoot ?? new Uint8Array(0),
        privateTripleCount: m.privateTripleCount ?? 0,
      })),
      publisherIdentity: keypairA.publicKey,
      publisherAddress: onChain.publisherAddress,
      startKAId: onChain.startKAId,
      endKAId: onChain.endKAId,
      chainId: chainA.chainId,
      publisherSignatureR: new Uint8Array(0),
      publisherSignatureVs: new Uint8Array(0),
    });

    const ackData = await routerA.send(
      nodeB.peerId,
      PROTOCOL_PUBLISH,
      publishRequest,
    );
    const ack = decodePublishAck(ackData);
    expect(ack.accepted).toBe(true);

    // === Step 3: Query on Node B ===
    const engineB = new DKGQueryEngine(storeB);
    const queryResult = await engineB.query(
      'SELECT ?name WHERE { ?s <http://schema.org/name> ?name }',
      { contextGraphId: CONTEXT_GRAPH },
    );

    expect(queryResult.bindings).toHaveLength(1);
    expect(queryResult.bindings[0]['name']).toBe('"ImageBot"');

    // Query for skills
    const skillResult = await engineB.query(
      'SELECT ?skill WHERE { ?s <http://ex.org/skill> ?skill }',
      { contextGraphId: CONTEXT_GRAPH },
    );
    expect(skillResult.bindings).toHaveLength(1);
    expect(skillResult.bindings[0]['skill']).toBe('"ImageAnalysis"');
  }, 20000);

  // RC11 / PR1: This test publishes with private quads and then uses
  // `result.onChainResult!` to construct the access UAL. Private-data
  // publishes intentionally skip peer ACK collection
  // (`dkg-publisher.ts:1937` — StorageACKHandler cannot recompute
  // private merkle roots from SWM data alone), and with the self-signed
  // ACK fallback deleted in PR1 the publisher now correctly downgrades
  // private-data publishes to `tentative` (no `onChainResult`). The
  // public-data access-protocol coverage in
  // `packages/publisher/test/access-protocol.test.ts` still exercises
  // the same denial / grant code paths on the confirmed-publish side;
  // the missing private-data coverage requires teaching the ACK
  // collector to carry `privateRoots` so cores can verify composite
  // roots without seeing the private data itself — out of scope for
  // PR1.
  it.skip('publishes with private triples and accesses them (skipped under RC11 / PR1: see comment above)', async () => {
    const nodeA = new DKGNode({
      listenAddresses: ['/ip4/127.0.0.1/tcp/0'],
      enableMdns: false,
    });
    const nodeB = new DKGNode({
      listenAddresses: ['/ip4/127.0.0.1/tcp/0'],
      enableMdns: false,
    });
    nodes.push(nodeA, nodeB);
    await nodeA.start();
    await nodeB.start();

    await nodeB.libp2p.dial(multiaddr(nodeA.multiaddrs[0]));
    await new Promise((r) => setTimeout(r, 500));

    const storeA = new OxigraphStore();
    const chainA = createEVMAdapter(HARDHAT_KEYS.CORE_OP);
    const busA = new TypedEventBus();
    const keypairA = await generateEd25519Keypair();
    const keypairB = await generateEd25519Keypair();

    // Publisher on A (holds private triples)
    const publisherA = makeTestPublisher({
      store: storeA,
      chain: chainA,
      eventBus: busA,
      keypair: keypairA,
      publisherPrivateKey: HARDHAT_KEYS.CORE_OP,
      publisherNodeIdentityId: BigInt(getSharedContext().coreProfileId),
    });

    // Publish with mixed public/private triples
    const result = await publisherA.publish({
      contextGraphId: CONTEXT_GRAPH,
      quads: [q(ENTITY, 'http://schema.org/name', '"ImageBot"')],
      privateQuads: [
        q(ENTITY, 'http://ex.org/apiKey', '"secret-key-xyz"'),
        q(ENTITY, 'http://ex.org/modelWeights', '"s3://bucket/weights.bin"'),
      ],
      publisherPeerId: nodeB.peerId,
    });

    expect(result.kaManifest[0].privateTripleCount).toBe(2);
    expect(result.status).toBe('confirmed');

    // Register access handler on A
    const accessHandler = new AccessHandler(storeA, busA);
    const routerA = new ProtocolRouter(nodeA);
    registerSubstrateHandler(routerA, PROTOCOL_ACCESS, async (data, peerId) => accessHandler.handler(data, { toString: () => peerId, toBytes: () => new Uint8Array() }));

    // AccessClient on B requests private triples from A
    const routerB = new ProtocolRouter(nodeB);
    const accessClient = new AccessClient(createSubstrateClient(routerB), keypairB, nodeB.peerId);

    const onChain = result.onChainResult!;
    const accessResult = await accessClient.requestAccess(
      nodeA.peerId,
      `did:dkg:evm:31337/${onChain.publisherAddress}/${onChain.startKAId}/1`,
    );

    expect(accessResult.granted).toBe(true);
    expect(accessResult.quads.length).toBe(2);

    const apiKeyTriple = accessResult.quads.find(
      (q) => q.predicate === 'http://ex.org/apiKey',
    );
    expect(apiKeyTriple).toBeDefined();
  }, 20000);
});

describe('Publisher wallet signature verification', () => {
  const signerWallet = ethers.Wallet.createRandom();
  const imposterWallet = ethers.Wallet.createRandom();
  const CHAIN_ID = 'evm:31337';

  function buildSignedRequest(
    quads: Quad[],
    wallet: ethers.Wallet,
    claimedAddress: string,
    startKAId: number,
    endKAId: number,
  ) {
    // Structured KC root (PoS content-binding): flatten all public quads into one
    // V10 tree with the private sibling collapsed to the sentinel (no private data
    // here) — identical to what PublishHandler recomputes, so confirmPublish matches.
    const merkleRoot = computeStructuredKCRootV10(quads, []).root;

    const nquads = quads.map(
      (q) => `<${q.subject}> <${q.predicate}> ${q.object.startsWith('"') ? q.object : `<${q.object}>`} <${q.graph}> .`,
    ).join('\n');

    return { merkleRoot, nquads, wallet, claimedAddress, startKAId, endKAId };
  }

  async function signCommitment(
    merkleRoot: Uint8Array,
    publisherAddress: string,
    startKAId: number,
    endKAId: number,
    chainId: string,
    contextGraphId: string,
    wallet: ethers.Wallet,
  ) {
    const commitHash = ethers.solidityPackedKeccak256(
      ['bytes32', 'address', 'uint64', 'uint64', 'string', 'string'],
      [ethers.hexlify(merkleRoot), publisherAddress, startKAId, endKAId, chainId, contextGraphId],
    );
    const rawSig = await wallet.signMessage(ethers.getBytes(commitHash));
    const { r, yParityAndS } = ethers.Signature.from(rawSig);
    return { r: ethers.getBytes(r), vs: ethers.getBytes(yParityAndS) };
  }

  it('accepts publish request with valid publisher wallet signature', async () => {
    const store = new OxigraphStore();
    const bus = new TypedEventBus();
    const handler = new PublishHandler(store, bus);

    const testQuads = [q(ENTITY, 'http://schema.org/name', '"SigBot"')];
    const { merkleRoot, nquads } = buildSignedRequest(
      testQuads, signerWallet, signerWallet.address, 1, 1,
    );

    const sig = await signCommitment(
      merkleRoot, signerWallet.address, 1, 1, CHAIN_ID, CONTEXT_GRAPH, signerWallet,
    );

    const reqBytes = encodePublishRequest({
      ual: `did:dkg:${CHAIN_ID}/${signerWallet.address}/1`,
      nquads: new TextEncoder().encode(nquads),
      contextGraphId: CONTEXT_GRAPH,
      kas: [{ tokenId: 1, rootEntity: ENTITY, privateMerkleRoot: new Uint8Array(0), privateTripleCount: 0 }],
      publisherIdentity: new Uint8Array(32),
      publisherAddress: signerWallet.address,
      startKAId: 1,
      endKAId: 1,
      chainId: CHAIN_ID,
      publisherSignatureR: sig.r,
      publisherSignatureVs: sig.vs,
    });

    const ackData = await handler.handler(reqBytes, 'test-peer' as any);
    const ack = decodePublishAck(ackData);
    expect(ack.accepted).toBe(true);
  });

  it('accepts publish request even with mismatched wallet sig (verified on-chain, not P2P)', async () => {
    const store = new OxigraphStore();
    const bus = new TypedEventBus();
    const handler = new PublishHandler(store, bus);

    const testQuads = [q(ENTITY, 'http://schema.org/name', '"SigBot"')];
    const { merkleRoot, nquads } = buildSignedRequest(
      testQuads, imposterWallet, signerWallet.address, 1, 1,
    );

    const sig = await signCommitment(
      merkleRoot, signerWallet.address, 1, 1, CHAIN_ID, CONTEXT_GRAPH, imposterWallet,
    );

    const reqBytes = encodePublishRequest({
      ual: `did:dkg:${CHAIN_ID}/${signerWallet.address}/1`,
      nquads: new TextEncoder().encode(nquads),
      contextGraphId: CONTEXT_GRAPH,
      kas: [{ tokenId: 1, rootEntity: ENTITY, privateMerkleRoot: new Uint8Array(0), privateTripleCount: 0 }],
      publisherIdentity: new Uint8Array(32),
      publisherAddress: signerWallet.address,
      startKAId: 1,
      endKAId: 1,
      chainId: CHAIN_ID,
      publisherSignatureR: sig.r,
      publisherSignatureVs: sig.vs,
    });

    // Publisher identity is now verified on-chain (msg.sender), not at the P2P layer.
    // The handler accepts the data tentatively; confirmation comes from chain events.
    const ackData = await handler.handler(reqBytes, 'test-peer' as any);
    const ack = decodePublishAck(ackData);
    expect(ack.accepted).toBe(true);
  });

  it('accepts publish request with no signature (optional for P2P-only mode)', async () => {
    const store = new OxigraphStore();
    const bus = new TypedEventBus();
    const handler = new PublishHandler(store, bus);

    const testQuads = [q('did:dkg:agent:QmNoSig', 'http://schema.org/name', '"NoSigBot"')];
    const { nquads } = buildSignedRequest(
      testQuads, signerWallet, signerWallet.address, 0, 0,
    );

    const reqBytes = encodePublishRequest({
      ual: `did:dkg:${CHAIN_ID}/${signerWallet.address}/0`,
      nquads: new TextEncoder().encode(nquads),
      contextGraphId: CONTEXT_GRAPH,
      kas: [{ tokenId: 1, rootEntity: 'did:dkg:agent:QmNoSig', privateMerkleRoot: new Uint8Array(0), privateTripleCount: 0 }],
      publisherIdentity: new Uint8Array(32),
      publisherAddress: '',
      startKAId: 0,
      endKAId: 0,
      chainId: CHAIN_ID,
      publisherSignatureR: new Uint8Array(0),
      publisherSignatureVs: new Uint8Array(0),
    });

    const ackData = await handler.handler(reqBytes, 'test-peer' as any);
    const ack = decodePublishAck(ackData);
    expect(ack.accepted).toBe(true);
  });

  it('confirmPublish verifies on-chain data matches expectations', async () => {
    const store = new OxigraphStore();
    const bus = new TypedEventBus();
    const handler = new PublishHandler(store, bus);

    const testQuads = [q('did:dkg:agent:QmConfirm', 'http://schema.org/name', '"ConfirmBot"')];
    const { merkleRoot, nquads } = buildSignedRequest(
      testQuads, signerWallet, signerWallet.address, 5, 5,
    );

    const sig = await signCommitment(
      merkleRoot, signerWallet.address, 5, 5, CHAIN_ID, CONTEXT_GRAPH, signerWallet,
    );

    const ual = `did:dkg:${CHAIN_ID}/${signerWallet.address}/5`;
    const reqBytes = encodePublishRequest({
      ual,
      nquads: new TextEncoder().encode(nquads),
      contextGraphId: CONTEXT_GRAPH,
      kas: [{ tokenId: 1, rootEntity: 'did:dkg:agent:QmConfirm', privateMerkleRoot: new Uint8Array(0), privateTripleCount: 0 }],
      publisherIdentity: new Uint8Array(32),
      publisherAddress: signerWallet.address,
      startKAId: 5,
      endKAId: 5,
      chainId: CHAIN_ID,
      publisherSignatureR: sig.r,
      publisherSignatureVs: sig.vs,
    });

    const ackData = await handler.handler(reqBytes, 'test-peer' as any);
    const ack = decodePublishAck(ackData);
    expect(ack.accepted).toBe(true);

    // Confirm with matching on-chain data
    const confirmed = await handler.confirmPublish(ual, {
      publisherAddress: signerWallet.address,
      merkleRoot,
      startKAId: 5n,
      endKAId: 5n,
    });
    expect(confirmed).toBe(true);
  });

  it('confirmPublish promotes tentative to confirmed in meta graph (no tentative left)', async () => {
    const store = new OxigraphStore();
    const bus = new TypedEventBus();
    const handler = new PublishHandler(store, bus);

    const testQuads = [q('did:dkg:agent:QmPromote', 'http://schema.org/name', '"PromoteBot"')];
    const { merkleRoot, nquads } = buildSignedRequest(
      testQuads, signerWallet, signerWallet.address, 7, 7,
    );

    const sig = await signCommitment(
      merkleRoot, signerWallet.address, 7, 7, CHAIN_ID, CONTEXT_GRAPH, signerWallet,
    );

    const ual = `did:dkg:${CHAIN_ID}/${signerWallet.address}/7`;
    const reqBytes = encodePublishRequest({
      ual,
      nquads: new TextEncoder().encode(nquads),
      contextGraphId: CONTEXT_GRAPH,
      kas: [{ tokenId: 1, rootEntity: 'did:dkg:agent:QmPromote', privateMerkleRoot: new Uint8Array(0), privateTripleCount: 0 }],
      publisherIdentity: new Uint8Array(32),
      publisherAddress: signerWallet.address,
      startKAId: 7,
      endKAId: 7,
      chainId: CHAIN_ID,
      publisherSignatureR: sig.r,
      publisherSignatureVs: sig.vs,
    });

    await handler.handler(reqBytes, 'test-peer' as any);

    const metaGraph = `did:dkg:context-graph:${CONTEXT_GRAPH}/_meta`;
    let statusResult = await store.query(
      `SELECT ?status WHERE { GRAPH <${metaGraph}> { <${ual}> <http://dkg.io/ontology/status> ?status } }`,
    );
    expect(statusResult.type).toBe('bindings');
    if (statusResult.type === 'bindings') {
      const statuses = statusResult.bindings.map((b) => b['status']);
      expect(statuses).toContainEqual(expect.stringMatching(/tentative/));
    }

    const confirmed = await handler.confirmPublish(ual, {
      publisherAddress: signerWallet.address,
      merkleRoot,
      startKAId: 7n,
      endKAId: 7n,
    });
    expect(confirmed).toBe(true);

    statusResult = await store.query(
      `SELECT ?status WHERE { GRAPH <${metaGraph}> { <${ual}> <http://dkg.io/ontology/status> ?status } }`,
    );
    expect(statusResult.type).toBe('bindings');
    if (statusResult.type === 'bindings') {
      const statuses = statusResult.bindings.map((b) => b['status']);
      expect(statuses).toHaveLength(1);
      expect(statuses[0]).toMatch(/confirmed/);
      expect(statuses.some((s) => s.includes('tentative'))).toBe(false);
    }
  });

  it('receiver recomputes same merkle root from parsed nquads as sender', async () => {
    const quads: Quad[] = [
      q('did:dkg:agent:QmRootMatch', 'http://schema.org/name', '"RootMatch"'),
      q('did:dkg:agent:QmRootMatch', 'http://schema.org/version', '"1"'),
    ];
    const { merkleRoot, nquads } = buildSignedRequest(
      quads, signerWallet, signerWallet.address, 11, 11,
    );

    const sig = await signCommitment(
      merkleRoot, signerWallet.address, 11, 11, CHAIN_ID, CONTEXT_GRAPH, signerWallet,
    );

    const reqBytes = encodePublishRequest({
      ual: `did:dkg:${CHAIN_ID}/${signerWallet.address}/11`,
      nquads: new TextEncoder().encode(nquads),
      contextGraphId: CONTEXT_GRAPH,
      kas: [
        { tokenId: 1, rootEntity: 'did:dkg:agent:QmRootMatch', privateMerkleRoot: new Uint8Array(0), privateTripleCount: 0 },
      ],
      publisherIdentity: new Uint8Array(32),
      publisherAddress: signerWallet.address,
      startKAId: 11,
      endKAId: 11,
      chainId: CHAIN_ID,
      publisherSignatureR: sig.r,
      publisherSignatureVs: sig.vs,
    });

    const request = decodePublishRequest(reqBytes);
    const nquadsStr = new TextDecoder().decode(request.nquads);
    const parsedQuads = parseSimpleNQuads(nquadsStr);

    // Same structured root the handler computes over the full payload.
    const recomputedRoot = computeStructuredKCRootV10(parsedQuads, []).root;

    expect(recomputedRoot).toEqual(merkleRoot);
  });

  it('confirmPublish rejects when on-chain publisher address mismatches', async () => {
    const store = new OxigraphStore();
    const bus = new TypedEventBus();
    const handler = new PublishHandler(store, bus);

    const testQuads = [q('did:dkg:agent:QmMismatch', 'http://schema.org/name', '"MismatchBot"')];
    const { merkleRoot, nquads } = buildSignedRequest(
      testQuads, signerWallet, signerWallet.address, 10, 10,
    );

    const sig = await signCommitment(
      merkleRoot, signerWallet.address, 10, 10, CHAIN_ID, CONTEXT_GRAPH, signerWallet,
    );

    const ual = `did:dkg:${CHAIN_ID}/${signerWallet.address}/10`;
    const reqBytes = encodePublishRequest({
      ual,
      nquads: new TextEncoder().encode(nquads),
      contextGraphId: CONTEXT_GRAPH,
      kas: [{ tokenId: 1, rootEntity: 'did:dkg:agent:QmMismatch', privateMerkleRoot: new Uint8Array(0), privateTripleCount: 0 }],
      publisherIdentity: new Uint8Array(32),
      publisherAddress: signerWallet.address,
      startKAId: 10,
      endKAId: 10,
      chainId: CHAIN_ID,
      publisherSignatureR: sig.r,
      publisherSignatureVs: sig.vs,
    });

    await handler.handler(reqBytes, 'test-peer' as any);

    // Try to confirm with a different publisher address
    const confirmed = await handler.confirmPublish(ual, {
      publisherAddress: imposterWallet.address,
      merkleRoot,
      startKAId: 10n,
      endKAId: 10n,
    });
    expect(confirmed).toBe(false);
  });
});
