import { ethers } from 'ethers';
import * as root from '../src';
import { v3 } from '../src';
import type {
  Address, Bytes32, CapabilityEvidence, EnforcementConfig,
  SignerIndependenceContext, V3DeploymentVerifier, V3NetworkProfile,
} from '../src/v3';

const address = (n: number): Address =>
  `0x${n.toString(16).padStart(40, '0')}` as Address;
const hash = (character: string): Bytes32 =>
  `0x${character.repeat(64)}` as Bytes32;

const V2_RUNTIME_EXPORTS = [
  'BASE_SEPOLIA_V3_CHAIN_ID',
  'BOND_CONTRACTS',
  'BaseSepoliaV3Rehearsal',
  'BondsModule',
  'ModuleType',
  'OBJECTIVE_RULE_SCHEMA_VERSION',
  'ObjectiveRuleClassV3',
  'TASK_CONTRACTS',
  'TasksModule',
  'TrustModule',
  'V3SAFE_NO_OBJECTIVE_RULE_SCHEMA_VERSION',
  'VaultfireSDK',
  'VaultfireSafeSDK',
  'assertObjectiveRuleV3',
  'canonicalArtifactEvidenceAttestationHash',
  'canonicalManifestContentHash',
  'createVaultfireSDK',
  'createVaultfireSafeSDK',
  'createVaultfireSafeSDKForChain',
  'parseCanonicalEvidenceJson',
  'validateBaseSepoliaV3Manifest',
  'validateBaseSepoliaV3ManifestJson',
];

describe('additive V3 namespace', () => {
  it('preserves the exact V2 runtime export snapshot and adds only v3', () => {
    expect(Object.keys(root).filter((name) => name !== 'v3').sort())
      .toEqual(V2_RUNTIME_EXPORTS);
    expect(root.v3).toBeDefined();
    expect((root as unknown as Record<string, unknown>).V3Client).toBeUndefined();
  });

  it('deep-freezes profiles, deployments, and route metadata', () => {
    const profile = v3.V3_NETWORK_PROFILES[8453];
    expect(Object.isFrozen(v3.V3_NETWORK_PROFILES)).toBe(true);
    expect(Object.isFrozen(profile)).toBe(true);
    expect(Object.isFrozen(profile.deployments)).toBe(true);
    expect(Object.isFrozen(profile.deployments.accountabilityBond)).toBe(true);
    expect(Object.isFrozen(v3.V3_ROUTE_METADATA)).toBe(true);
    expect(Object.isFrozen(v3.V3_RETIRED_ROUTES.v2Fallback.retirement)).toBe(true);
    expect(v3.V3_RETIRED_ROUTES.latest).toMatchObject({
      disposition: 'retired',
      retirement: { replacement: null },
    });
  });

  it('has explicit unconfigured Base profiles and no fallback profile', () => {
    expect(Object.keys(v3.V3_NETWORK_PROFILES).sort()).toEqual(['8453', '84532']);
    for (const profile of Object.values(v3.V3_NETWORK_PROFILES)) {
      expect(profile.status).toBe('unconfigured');
      for (const deployment of Object.values(profile.deployments)) {
        expect(deployment.address).toBeNull();
        expect(deployment.abiIdentity).toMatch(/^0x[0-9a-f]{64}$/);
        expect(deployment.runtimeCodeHash).toBeNull();
      }
    }
  });
});

describe('canonical V3 hashing parity vectors', () => {
  const attesters = [address(1), address(2)] as const;
  const adjudicators = [address(4), address(5)] as const;
  const enforcement: EnforcementConfig = {
    canonicalSource: address(3),
    canonicalSourceCodehash: hash('0'),
    canonicalSourceId: hash('1'),
    obligationId: hash('2'),
    requiredBy: 1700000100n,
    responseWindow: 3600n,
    challengeWindow: 7200n,
    decisionWindow: 10800n,
    maximumSlash: 500n,
    evidenceSchemaHash: v3.OBJECTIVE_EVIDENCE_SCHEMA_HASH,
    privacyExclusionsHash: v3.PRIVACY_EXCLUSIONS_HASH,
    attesterSetHash: hash('0'),
    adjudicatorSetHash: hash('0'),
    attesterThreshold: 0,
    adjudicatorThreshold: 0,
  };
  const ruleHash = v3.hashEnforcementConfig(enforcement);
  const independence: SignerIndependenceContext = {
    aiCompany: address(10),
    counterparty: address(11),
    owner: address(12),
    consensus: address(13),
    disputeRegistry: address(14),
    canonicalSource: address(3),
    pilotController: address(15),
  };
  const termsHash = v3.hashAccountabilityTerms({
    bondId: 7n,
    aiCompany: address(1),
    counterparty: address(2),
    companyStake: 1000n,
    stakeAsset: ethers.ZeroAddress as Address,
    objectiveRuleHash: ruleHash,
    objectiveRuleClass: v3.ObjectiveRuleClass.MissedCanonicalDeadline,
    objectiveRuleSchemaVersion: 1,
    activePeriodStart: 1700000000n,
    activePeriodEnd: 1700100000n,
    challengeWindow: 7200n,
    nonce: 9n,
    chainId: 8453,
    protocolVersion: '3.0.0',
  });
  const evidenceHash = v3.hashObjectiveEvidence({
    bond: address(3),
    chainId: 8453,
    bondId: 7n,
    termsHash,
    ruleHash,
    canonicalSource: address(3),
    canonicalSourceId: hash('1'),
    obligationId: hash('2'),
    requiredBy: 1700000100n,
    recordedAt: 1700000101n,
    observedAt: 1700000200n,
    nonce: 10n,
    signatureDeadline: 1700000300n,
  });

  it('matches AccountabilityBondV3Safe ABI-encoding vectors', () => {
    expect(ruleHash).toBe('0x96374d01eba912adab55bd130bc8a9cd818d0543712b254efe30ff48930f70f1');
    expect(termsHash).toBe('0xa50986026ba3cd75f10d7e1af28ab7ea9792016b4df346e93aee4a9205954c03');
    expect(evidenceHash).toBe('0x8167e19ca95260bc4da3f52906252f1072ab97a22eb541e232e13115ac50e5e1');
    const domain = v3.domainSeparator(8453, address(3));
    expect(domain).toBe('0xd3717fc00b02671081390c7b91f252ba1b3a2e77044b4008c299fba5af064acd');
    expect(v3.typedDataDigest(domain, evidenceHash))
      .toBe('0x22e814304e3ef4ece9eff9ce160c555a9d512bb9ae4d69b868cd0370acc65804');
    const resolutionHash = v3.hashTypedResolution({
      bond: address(3),
      chainId: 8453,
      bondId: 7n,
      termsHash,
      ruleHash,
      evidenceHash: v3.typedDataDigest(domain, evidenceHash),
      challengeHash: hash('3'),
      evidenceValid: true,
      nonce: 11n,
      deadline: 1700001000n,
    });
    expect(resolutionHash).toBe('0x1678730f4d436243200a909a2f65fdfa427293c6ce07a99a121f16e052be7dc1');
    expect(v3.typedDataDigest(domain, resolutionHash))
      .toBe('0xbeb6ba20442ed6ae3a4ac68654e5f862a6bdc464cead9772ef3e8886844eedd4');
  });

  it('matches PartnershipBondV3Safe bilateral settlement vectors', () => {
    const partnershipTermsHash = v3.hashPartnershipTerms({
      bondId: 8n,
      partyA: address(1),
      partyB: address(2),
      partyAStake: 400n,
      partyBStake: 600n,
      stakeAsset: ethers.ZeroAddress as Address,
      partnershipType: hash('4'),
      objectiveRuleClass: 0,
      objectiveRuleSchemaVersion: 0,
      proposalDeadline: 1700000000n,
      activePeriodStart: 1700000100n,
      activePeriodEnd: 1700100000n,
      challengeWindow: 7200n,
      nonce: 12n,
      chainId: 84532,
      protocolVersion: '3.0.0',
    });
    expect(partnershipTermsHash)
      .toBe('0xc25582b44193475b0a258306a38ed4db9df25bdb03c5525f4946b304926f0dbb');
    const settlementHash = v3.hashPartnershipMutualSettlement({
      bond: address(3),
      chainId: 84532,
      bondId: 8n,
      termsHash: partnershipTermsHash,
      partyAAmount: 450n,
      partyBAmount: 550n,
      nonce: 13n,
      deadline: 1700000900n,
    });
    expect(settlementHash)
      .toBe('0x61e0954d272710978a7aaaa4d64a557e8d588afc399a384f506118d9986b7bf8');
    expect(v3.typedDataDigest(v3.domainSeparator(84532, address(3)), settlementHash))
      .toBe('0x08a88830e4436f7eb1b7cad253427b61d738209420421268d61d3edeec4af3d6');
  });

  it('matches the current empty-set and zero-threshold Solidity signer semantics', () => {
    expect(() => v3.validateSignerConfig({
      attesters: [],
      adjudicators: [],
    }, enforcement, independence))
      .not.toThrow();
    for (const [config, changed] of [
      [{ attesters, adjudicators: [] }, {}],
      [{ attesters: [], adjudicators }, {}],
      [{ attesters: [], adjudicators: [] }, { attesterThreshold: 1 }],
      [{ attesters: [], adjudicators: [] }, { adjudicatorThreshold: 1 }],
      [{ attesters: [], adjudicators: [] }, { attesterSetHash: hash('1') }],
      [{ attesters: [], adjudicators: [] }, { adjudicatorSetHash: hash('1') }],
    ] as const) {
      expect(() => v3.validateSignerConfig(
        config,
        { ...enforcement, ...changed },
        independence,
      )).toThrow(/requires empty signer sets/);
    }
  });
});


describe('canonical supported V3 module boundary', () => {
  it('publishes only modules selected from the canonical support closure', () => {
    expect(v3.V3_SUPPORTED_DEPLOYMENT_NAMES).toEqual([
      'accountabilityBond',
      'partnershipBond',
      'governance',
      'mandateRegistry',
    ]);
    expect(Object.keys(v3.V3_ABI_IDENTITIES)).toEqual(v3.V3_SUPPORTED_DEPLOYMENT_NAMES);
    expect(Object.keys(v3.V3_WRITE_METHODS)).toEqual(v3.V3_SUPPORTED_DEPLOYMENT_NAMES);
    expect((v3.V3_ABI_IDENTITIES as Record<string, unknown>).taskEscrow).toBeUndefined();
    expect((v3.V3_WRITE_METHODS as Record<string, unknown>).taskEscrow).toBeUndefined();
    expect(v3.V3_ROUTE_METADATA.taskEscrow).toMatchObject({
      disposition: 'retired',
      retirement: { replacement: null },
    });
  });

  it('rejects retired TaskEscrow writes and reads before any verifier, adapter, or sender call', async () => {
    const calls: string[] = [];
    const client = new v3.V3Client({
      profile: configuredCallerProfile(),
      readTransport: { read: async () => { calls.push('read'); return {}; } },
      writeTransport: { sendTransaction: async () => { calls.push('send'); return hash('c'); } },
      deploymentVerifier: verifier(calls),
      capabilityVerifier: { verify: async () => { calls.push('capability'); return true; } },
      now: () => 150n,
    });
    expect((client as unknown as Record<string, unknown>).readTaskEscrow).toBeUndefined();
    await expect(client.write({
      ...canonicalRequest(),
      deployment: 'taskEscrow',
      value: 7n,
    } as unknown as v3.V3WriteRequest)).rejects.toMatchObject({ code: 'V3_WRITES_BLOCKED' });
    await expect((client as unknown as {
      read(route: string, query: Record<string, unknown>): Promise<unknown>;
    }).read('taskEscrow', {})).rejects.toMatchObject({ code: 'V3_UNAVAILABLE' });
    expect(calls).toEqual([]);
  });
});

const deploymentAddress = address(9);
const runtimeCode = '0x60006000' as const;
const runtimeCodeHash = ethers.keccak256(runtimeCode) as Bytes32;
const method = v3.V3_WRITE_METHODS.accountabilityBond
  .find((candidate) => candidate.signature === 'fundNative(uint256)')!;

function configuredCallerProfile(): V3NetworkProfile {
  return {
    chainId: 8453,
    name: 'Base',
    status: 'configured',
    deployments: {
      accountabilityBond: {
        address: deploymentAddress,
        abiIdentity: v3.V3_ABI_IDENTITIES.accountabilityBond,
        runtimeCodeHash,
      },
      partnershipBond: {
        address: null,
        abiIdentity: v3.V3_ABI_IDENTITIES.partnershipBond,
        runtimeCodeHash: null,
      },
      governance: {
        address: null,
        abiIdentity: v3.V3_ABI_IDENTITIES.governance,
        runtimeCodeHash: null,
      },
      mandateRegistry: {
        address: null,
        abiIdentity: v3.V3_ABI_IDENTITIES.mandateRegistry,
        runtimeCodeHash: null,
      },
    },
  };
}

function verifier(calls: string[], overrides: Partial<V3DeploymentVerifier> = {}): V3DeploymentVerifier {
  return {
    getVerifiedChainId: async () => { calls.push('chain'); return 8453n; },
    getAbiIdentity: async () => { calls.push('abi'); return v3.V3_ABI_IDENTITIES.accountabilityBond; },
    getRuntimeCode: async () => { calls.push('code'); return runtimeCode; },
    ...overrides,
  };
}

function canonicalRequest(): v3.V3WriteRequest {
  const subject = address(8);
  return {
    deployment: 'accountabilityBond',
    capability: method.capability,
    subject,
    data: new ethers.Interface([`function ${method.signature}`])
      .encodeFunctionData(method.signature, [1n]) as `0x${string}`,
    value: 1n,
    evidence: {
      schema: 'vaultfire.v3.capability-evidence.v1',
      chainId: 8453,
      contract: deploymentAddress,
      capability: method.capability,
      subject,
      issuedAt: 100n,
      expiresAt: 200n,
      evidenceHash: hash('b'),
    },
  };
}

describe('runtime-bound fail-closed deployment authorization', () => {
  it('rejects caller-selected configured profiles because canonical profiles are unconfigured', async () => {
    const calls: string[] = [];
    const client = new v3.V3Client({
      profile: configuredCallerProfile(),
      writeTransport: { sendTransaction: async () => { calls.push('send'); return hash('c'); } },
      deploymentVerifier: verifier(calls),
      capabilityVerifier: { verify: async () => { calls.push('capability'); return true; } },
      now: () => 150n,
    });
    await expect(client.write(canonicalRequest())).rejects.toMatchObject({
      code: 'V3_WRITES_BLOCKED',
      cause: expect.objectContaining({ message: expect.stringMatching(/canonically authorized/) }),
    });
    expect(calls).toEqual([]);
  });

  it('requires exact independently observed runtime bytecode in the internal deployment gate', async () => {
    const { verifyAuthorizedDeployment } = await import('../src/v3/internal/deployment');
    const configured = configuredCallerProfile();
    const canonical = configuredCallerProfile();
    const cases: Array<{
      label: string;
      profile?: V3NetworkProfile;
      canonical?: V3NetworkProfile;
      verifier: V3DeploymentVerifier;
    }> = [
      {
        label: 'no code',
        verifier: verifier([], { getRuntimeCode: async () => null }),
      },
      {
        label: 'empty code',
        verifier: verifier([], { getRuntimeCode: async () => '0x' }),
      },
      {
        label: 'same ABI different code',
        verifier: verifier([], { getRuntimeCode: async () => '0x6001' }),
      },
      {
        label: 'proxy drift',
        verifier: verifier([], { getRuntimeCode: async () => '0x366000' }),
      },
      {
        label: 'wrong ABI',
        verifier: verifier([], { getAbiIdentity: async () => hash('f') }),
      },
      {
        label: 'wrong chain',
        verifier: verifier([], { getVerifiedChainId: async () => 84532n }),
      },
      {
        label: 'caller-selected address',
        profile: {
          ...configured,
          deployments: {
            ...configured.deployments,
            accountabilityBond: {
              ...configured.deployments.accountabilityBond,
              address: address(99),
            },
          },
        },
        verifier: verifier([]),
      },
      {
        label: 'caller-selected code hash',
        profile: {
          ...configured,
          deployments: {
            ...configured.deployments,
            accountabilityBond: {
              ...configured.deployments.accountabilityBond,
              runtimeCodeHash: hash('f'),
            },
          },
        },
        verifier: verifier([]),
      },
    ];
    for (const entry of cases) {
      await expect(verifyAuthorizedDeployment(
        entry.profile ?? configured,
        entry.canonical ?? canonical,
        'accountabilityBond',
        v3.V3_ABI_IDENTITIES.accountabilityBond,
        entry.verifier,
      )).rejects.toThrow();
    }
    await expect(verifyAuthorizedDeployment(
      configured,
      canonical,
      'accountabilityBond',
      v3.V3_ABI_IDENTITIES.accountabilityBond,
      verifier([]),
    )).resolves.toBe(deploymentAddress);
  });

  it('blocks reads before adapter invocation when no canonical runtime deployment is authorized', async () => {
    const calls: string[] = [];
    const baseProfile = configuredCallerProfile();
    const profile: V3NetworkProfile = {
      ...baseProfile,
      deployments: {
        ...baseProfile.deployments,
        governance: {
          address: address(20),
          abiIdentity: v3.V3_ABI_IDENTITIES.governance,
          runtimeCodeHash,
        },
      },
    };
    const client = new v3.V3Client({
      profile,
      readTransport: { read: async () => { calls.push('read'); return {}; } },
      deploymentVerifier: verifier(calls),
    });
    await expect(client.readGovernance({})).rejects.toMatchObject({ code: 'V3_UNAVAILABLE' });
    expect(calls).toEqual([]);
  });
});

describe('canonical data hardening', () => {
  it('rejects every class of extra capability-evidence field before verifier or sender calls', async () => {
    const variants: unknown[] = [];
    variants.push({ ...canonicalRequest().evidence, unexpected: true });
    const withSymbol = { ...canonicalRequest().evidence } as Record<PropertyKey, unknown>;
    withSymbol[Symbol('hostile')] = true;
    variants.push(withSymbol);
    const nonEnumerable = { ...canonicalRequest().evidence };
    Object.defineProperty(nonEnumerable, 'hidden', { value: true, enumerable: false });
    variants.push(nonEnumerable);
    const accessor = { ...canonicalRequest().evidence };
    Object.defineProperty(accessor, 'hidden', { get: () => true, enumerable: true });
    variants.push(accessor);

    for (const evidence of variants) {
      const calls: string[] = [];
      const client = new v3.V3Client({
        profile: configuredCallerProfile(),
        writeTransport: { sendTransaction: async () => { calls.push('send'); return hash('c'); } },
        deploymentVerifier: verifier(calls),
        capabilityVerifier: { verify: async () => { calls.push('capability'); return true; } },
        now: () => 150n,
      });
      await expect(client.write({
        ...canonicalRequest(),
        evidence,
      } as v3.V3WriteRequest)).rejects.toMatchObject({ code: 'V3_WRITES_BLOCKED' });
      expect(calls).toEqual([]);
    }
  });

  it('rejects symbol and non-enumerable fields before clone normalization', () => {
    const symbol = Symbol('hostile-extra');
    expect(() => v3.cloneAndDeepFreeze({ valid: true, [symbol]: { hostile: true } }))
      .toThrow(/symbol/);
    const hidden = { valid: true };
    Object.defineProperty(hidden, 'hostile', { value: true, enumerable: false });
    expect(() => v3.cloneAndDeepFreeze(hidden)).toThrow(/non-enumerable/);
  });

  it('rejects the crafted custom-prototype replica while preserving ordinary cycle support', () => {
    const fakePrototype = Object.create(null);
    for (const key of Reflect.ownKeys(Object.prototype)) {
      Object.defineProperty(
        fakePrototype,
        key,
        Object.getOwnPropertyDescriptor(Object.prototype, key)!,
      );
    }
    const crafted = Object.create(fakePrototype);
    crafted.value = { mutable: true };
    expect(() => v3.deepFreeze(crafted)).toThrow(/plain records/);

    const cyclic: { self?: unknown } = {};
    cyclic.self = cyclic;
    expect(() => v3.deepFreeze(cyclic)).not.toThrow();
    expect(Object.isFrozen(cyclic)).toBe(true);
  });
});
