import { describe, expect, it } from 'vitest';
import { ethers } from 'ethers';
import {
  computeGossipSigningPayload,
  decodeGossipEnvelope,
  decodeSwmSenderKeyMessage,
  decodeWorkspacePublishRequest,
  DKG_ONTOLOGY,
  GOSSIP_TYPE_WORKSPACE_PUBLISH,
  GOSSIP_ENVELOPE_VERSION,
  contextGraphDataUri,
  contextGraphMetaUri,
  contextGraphWorkspaceTopic,
  SWM_SENDER_KEY_MESSAGE_TYPE,
  type OperationContext,
  type SwmSenderKeyMessageMsg,
} from '@origintrail-official/dkg-core';
import { MockChainAdapter } from '@origintrail-official/dkg-chain';
import { DKGAgent, agentFromPrivateKey, type AgentKeyRecord } from '../src/index.js';

interface DKGAgentInternals {
  localAgents: Map<string, AgentKeyRecord>;
  defaultAgentAddress?: string;
  encodeWorkspaceGossipMessage(contextGraphId: string, message: Uint8Array): Promise<Uint8Array>;
  decryptWorkspacePayloadWithSenderKey(
    message: SwmSenderKeyMessageMsg,
    contextGraphId: string,
    ctx: OperationContext,
  ): Promise<Uint8Array>;
}

class CapturingGossip {
  messages: Array<{ topic: string; data: Uint8Array }> = [];

  subscribe(_topic: string): void {}

  unsubscribe(_topic: string): void {}

  onMessage(_topic: string, _handler: (topic: string, data: Uint8Array, from?: string) => void | Promise<void>): void {}

  async publish(topic: string, data: Uint8Array): Promise<void> {
    this.messages.push({ topic, data });
  }

  // rc.9 PR-C: tier-switch in publishWorkspaceGossip consults
  // GossipSubManager.getSubscribers. Empty roster keeps these
  // tests focused on the gossip-signing path (no substrate
  // fan-out competes for the encoded message bytes).
  getSubscribers(_topic: string): string[] { return []; }
}

async function insertAgentGate(
  agent: DKGAgent,
  contextGraphId: string,
  predicate: string,
  agentAddress: string,
): Promise<void> {
  await agent.store.insert([{
    subject: contextGraphDataUri(contextGraphId),
    predicate,
    object: `"${agentAddress}"`,
    graph: contextGraphMetaUri(contextGraphId),
  }]);
}

function expectSignedEnvelope(
  wireMessage: Uint8Array,
  contextGraphId: string,
  payload: Uint8Array,
  expectedAgentAddress: string,
): void {
  const envelope = decodeGossipEnvelope(wireMessage);

  expect(envelope.version).toBe(GOSSIP_ENVELOPE_VERSION);
  expect(envelope.type).toBe(GOSSIP_TYPE_WORKSPACE_PUBLISH);
  expect(envelope.contextGraphId).toBe(contextGraphId);
  expect(envelope.agentAddress).toBe(expectedAgentAddress);
  expect(Array.from(envelope.payload)).toEqual(Array.from(payload));

  const recovered = ethers.verifyMessage(
    computeGossipSigningPayload(envelope.type, envelope.contextGraphId, envelope.timestamp, envelope.payload),
    ethers.hexlify(envelope.signature),
  );
  expect(recovered).toBe(expectedAgentAddress);
}

describe('DKGAgent SWM gossip signing', () => {
  it('wraps open-graph SWM gossip with a local agent key even when the default cannot sign', async () => {
    const agent = await DKGAgent.create({
      name: 'SwmFallbackSigner',
      chainAdapter: new MockChainAdapter(),
    });
    const internals = agent as unknown as DKGAgentInternals;

    const defaultRecord = agentFromPrivateKey(ethers.Wallet.createRandom().privateKey, 'default');
    delete defaultRecord.privateKey;
    const fallbackRecord = agentFromPrivateKey(ethers.Wallet.createRandom().privateKey, 'fallback');

    internals.localAgents.set(defaultRecord.agentAddress, defaultRecord);
    internals.localAgents.set(fallbackRecord.agentAddress, fallbackRecord);
    internals.defaultAgentAddress = defaultRecord.agentAddress;

    const contextGraphId = 'open-swm-cg';
    const payload = new TextEncoder().encode('raw shared-memory payload');
    const wireMessage = await internals.encodeWorkspaceGossipMessage(contextGraphId, payload);
    expectSignedEnvelope(wireMessage, contextGraphId, payload, fallbackRecord.agentAddress);
  });

  it.each([
    ['DKG_ALLOWED_AGENT', DKG_ONTOLOGY.DKG_ALLOWED_AGENT],
    ['DKG_PARTICIPANT_AGENT', DKG_ONTOLOGY.DKG_PARTICIPANT_AGENT],
  ])('wraps agent-gated SWM gossip with the local %s key', async (_label, predicate) => {
    const agent = await DKGAgent.create({
      name: 'SwmGatedSigner',
      chainAdapter: new MockChainAdapter(),
    });
    const internals = agent as unknown as DKGAgentInternals;

    const defaultRecord = agentFromPrivateKey(ethers.Wallet.createRandom().privateKey, 'default');
    const gatedRecord = agentFromPrivateKey(ethers.Wallet.createRandom().privateKey, 'gated');

    internals.localAgents.set(defaultRecord.agentAddress, defaultRecord);
    internals.localAgents.set(gatedRecord.agentAddress, gatedRecord);
    internals.defaultAgentAddress = defaultRecord.agentAddress;

    const contextGraphId = `gated-swm-cg-${predicate.endsWith('allowedAgent') ? 'allowed' : 'participant'}`;
    await insertAgentGate(agent, contextGraphId, predicate, gatedRecord.agentAddress);

    const payload = new TextEncoder().encode('gated shared-memory payload');
    const wireMessage = await internals.encodeWorkspaceGossipMessage(contextGraphId, payload);

    expectSignedEnvelope(wireMessage, contextGraphId, payload, gatedRecord.agentAddress);
  });

  it('rejects outgoing agent-gated SWM gossip when no local allowed signing key exists', async () => {
    const agent = await DKGAgent.create({
      name: 'SwmGatedNoSigner',
      chainAdapter: new MockChainAdapter(),
    });
    const internals = agent as unknown as DKGAgentInternals;

    const localRecord = agentFromPrivateKey(ethers.Wallet.createRandom().privateKey, 'local');
    const remoteAllowed = ethers.Wallet.createRandom();
    internals.localAgents.set(localRecord.agentAddress, localRecord);
    internals.defaultAgentAddress = localRecord.agentAddress;

    const contextGraphId = 'gated-swm-cg-no-local-signer';
    await insertAgentGate(agent, contextGraphId, DKG_ONTOLOGY.DKG_ALLOWED_AGENT, remoteAllowed.address);

    const payload = new TextEncoder().encode('gated payload without a local signer');
    await expect(internals.encodeWorkspaceGossipMessage(contextGraphId, payload))
      .rejects.toThrow(/no local allowed signing agent key/);
  });

  it('encrypts agent-gated SWM wire payloads as a Sender Key broadcast', async () => {
    const agent = await DKGAgent.create({
      name: 'SwmEncryptedWire',
      chainAdapter: new MockChainAdapter(),
    });
    const internals = agent as unknown as DKGAgentInternals;
    const gossip = new CapturingGossip();
    (agent as unknown as { gossip: CapturingGossip }).gossip = gossip;
    Object.defineProperty((agent as unknown as { node: object }).node, 'peerId', {
      value: { toString: () => '12D3KooWEncryptedWireLocal' },
      configurable: true,
    });

    const allowedRecord = await agent.registerAgent('allowed-encryption-recipient');
    internals.defaultAgentAddress = allowedRecord.agentAddress;
    const contextGraphId = 'gated-swm-encrypted-wire';
    await insertAgentGate(agent, contextGraphId, DKG_ONTOLOGY.DKG_ALLOWED_AGENT, allowedRecord.agentAddress);

    await agent.share(contextGraphId, [{
      subject: 'urn:test:encrypted-wire',
      predicate: 'http://schema.org/name',
      object: '"wire secret"',
      graph: '',
    }]);

    expect(gossip.messages).toHaveLength(1);
    // OT-RFC-38 / LU-6 Phase B: gossip topic uses the wire-form
    // (hash) id so hosting cores can auto-subscribe via the chain-
    // event path without learning cleartext. Envelope itself still
    // carries cleartext in `contextGraphId` (privacy compromise — see
    // `publishWorkspaceGossip` comment).
    const expectedTopic = contextGraphWorkspaceTopic(
      ethers.keccak256(ethers.toUtf8Bytes(contextGraphId)).toLowerCase(),
    );
    expect(gossip.messages[0]?.topic).toBe(expectedTopic);
    const envelope = decodeGossipEnvelope(gossip.messages[0]!.data);
    expect(envelope.agentAddress).toBe(allowedRecord.agentAddress);
    let decodedAsPlainWorkspace = false;
    try {
      const request = decodeWorkspacePublishRequest(envelope.payload);
      decodedAsPlainWorkspace = request.paranetId === contextGraphId &&
        new TextDecoder().decode(request.nquads).includes('wire secret');
    } catch {
      decodedAsPlainWorkspace = false;
    }
    expect(decodedAsPlainWorkspace).toBe(false);

    const senderKeyMessage = decodeSwmSenderKeyMessage(envelope.payload);
    expect(senderKeyMessage.type).toBe(SWM_SENDER_KEY_MESSAGE_TYPE);
    expect(senderKeyMessage.contextGraphId).toBe(contextGraphId);
    expect(senderKeyMessage.senderAgentAddress).toBe(allowedRecord.agentAddress);
    expect(senderKeyMessage.ciphertext.length).toBeGreaterThan(0);
    expect(senderKeyMessage).not.toHaveProperty('recipients');
    const wireText = Buffer.from(envelope.payload).toString('utf8');
    expect(wireText).not.toContain('wire secret');

    const decrypted = await internals.decryptWorkspacePayloadWithSenderKey(
      senderKeyMessage,
      contextGraphId,
      { operationId: 'test', operationName: 'share' },
    );
    const request = decodeWorkspacePublishRequest(decrypted);
    expect(new TextDecoder().decode(request.nquads)).toContain('wire secret');
  });

  it('keeps legacy raw SWM gossip for open graphs when no local signing key exists', async () => {
    const agent = await DKGAgent.create({
      name: 'SwmOpenNoSigner',
      chainAdapter: new MockChainAdapter(),
    });
    const internals = agent as unknown as DKGAgentInternals;

    const contextGraphId = 'open-swm-cg-no-signer';
    const payload = new TextEncoder().encode('legacy raw shared-memory payload');
    const wireMessage = await internals.encodeWorkspaceGossipMessage(contextGraphId, payload);

    expect(Array.from(wireMessage)).toEqual(Array.from(payload));
  });
});
