import { execFileSync } from 'child_process';
import { createHash } from 'crypto';
import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
import { join, relative } from 'path';
import { ethers } from 'ethers';
import { v3 } from '../src';

const root = join(__dirname, '..');
const dist = join(root, 'dist');

const declarationFiles = (directory: string): string[] =>
  readdirSync(directory).flatMap((entry) => {
    const path = join(directory, entry);
    return statSync(path).isDirectory()
      ? declarationFiles(path)
      : path.endsWith('.d.ts') ? [path] : [];
  });

const distDigest = (): string => {
  const hash = createHash('sha256');
  const files = readdirSync(dist, { recursive: true })
    .map(String)
    .filter((file) => /\.(?:js|d\.ts)$/.test(file))
    .sort();
  for (const file of files) {
    hash.update(file);
    hash.update('\0');
    hash.update(readFileSync(join(dist, file)));
    hash.update('\0');
  }
  return hash.digest('hex');
};

describe('V3 generated public API', () => {
  beforeAll(() => {
    execFileSync('npm', ['run', 'build', '--silent'], { cwd: root });
  });

  it('contains no prohibited compatibility symbols or implementation placeholders', () => {
    const files = declarationFiles(join(dist, 'v3'));
    expect(files.length).toBeGreaterThan(0);
    const publicApi = files.map((file) =>
      `// ${relative(dist, file)}\n${readFileSync(file, 'utf8')}`).join('\n');
    for (const symbol of [
      'missionHash',
      'objectiveBreachRulesHash',
      'flourishRewardRulesHash',
      'flourishRewardConditionsHash',
      'rewardProgramId',
      'TaskEscrowReadModel',
      'TaskLifecycle',
      'readTaskEscrow',
      'createTaskNative',
      'wire to',
      'TODO',
    ]) {
      expect(publicApi).not.toContain(symbol);
    }
  });

  it('builds byte-for-byte deterministically', () => {
    const first = distDigest();
    execFileSync('npm', ['run', 'build', '--silent'], { cwd: root });
    expect(distDigest()).toBe(first);
  });

  it('derives every supported ABI identity from the reviewed artifact signatures', () => {
    const fixture = JSON.parse(readFileSync(
      join(root, 'tests', 'fixtures', 'v3-reviewed-abi-signatures.json'),
      'utf8',
    ));
    expect(fixture.schema).toBe('vaultfire.v3.reviewed-abi-signatures.v1');
    const artifactRoot = join(root, '..', '..', 'v3-src', 'out');
    for (const [name, reviewed] of Object.entries(fixture.deployments) as [
      keyof typeof v3.V3_ABI_IDENTITIES,
      {
        artifact: string;
        identity: string;
        functions: { signature: string; stateMutability: string }[];
      },
    ][]) {
      const artifactPath = join(artifactRoot, reviewed.artifact);
      const snapshotPath = join(
        root,
        'tests',
        'fixtures',
        'reviewed-artifacts',
        reviewed.artifact.split('/').pop()!,
      );
      const artifact = JSON.parse(readFileSync(
        existsSync(artifactPath) ? artifactPath : snapshotPath,
        'utf8',
      ));
      const artifactFunctions = new ethers.Interface(artifact.abi).fragments
        .filter((fragment): fragment is ethers.FunctionFragment => fragment.type === 'function')
        .map((fragment) => ({
          signature: fragment.format('sighash'),
          stateMutability: fragment.stateMutability,
        }))
        .sort((left, right) =>
          left.signature < right.signature ? -1 : left.signature > right.signature ? 1 : 0);
      expect(reviewed.functions).toEqual(artifactFunctions);
      const signatures = reviewed.functions.map((method) => method.signature);
      const key = name;
      expect(v3.V3_ABI_IDENTITIES[key])
        .toBe(ethers.keccak256(ethers.toUtf8Bytes(signatures.join('\n'))));
      expect(v3.V3_ABI_IDENTITIES[key]).toBe(reviewed.identity);
      for (const method of v3.V3_WRITE_METHODS[key]) {
        expect(signatures).toContain(method.signature);
        expect(method.selector).toBe(ethers.id(method.signature).slice(0, 10));
        const reviewedMethod = reviewed.functions.find(
          (candidate) => candidate.signature === method.signature,
        );
        expect(method.payable).toBe(reviewedMethod?.stateMutability === 'payable');
      }
    }
    expect(fixture.deployments.accountabilityBond.functions).toHaveLength(36);
    expect(fixture.deployments.accountabilityBond.functions.map(
      (method: { signature: string }) => method.signature,
    )).toEqual(expect.arrayContaining([
      'FLOURISHING_NON_CONTROL_BOUNDARY()',
      'advanceObjectiveEvidence(uint256)',
    ]));
    const accountabilitySignatures = fixture.deployments.accountabilityBond.functions.map(
      (method: { signature: string }) => method.signature,
    );
    for (const removedSignature of [
      'advanceToFinalizable(uint256)',
      'attestBreach(uint256,uint256,bytes32,uint96)',
      'breachSnapshot(uint256,uint256,bytes32)',
      'cancelStalledSettlement(uint256)',
      'challengeSettlement(uint256,uint256)',
      'disputeSubject(uint256)',
      'freezeSettlement(uint256,uint96)',
      'releaseDisputePayout(uint256,address,uint256)',
      'resolveChallenge(uint256)',
      'settle(uint256)',
    ]) {
      expect(accountabilitySignatures).not.toContain(removedSignature);
    }
    expect(v3.V3_WRITE_METHODS.accountabilityBond.map((method) => method.signature))
      .toContain('advanceObjectiveEvidence(uint256)');
    expect(v3.V3_ABI_IDENTITIES.accountabilityBond)
      .toBe('0x0c18d05c868716fad4bfc1fcfad2da51c83f247de06a7e3871dcece768a9526f');
    expect(fixture.deployments.partnershipBond.functions).toHaveLength(55);
    expect(fixture.deployments.partnershipBond.functions.map(
      (method: { signature: string }) => method.signature,
    )).toEqual(expect.arrayContaining([
      'FLOURISHING_MISSION()',
      'FLOURISHING_NON_CONTROL_BOUNDARY()',
    ]));
    expect(v3.V3_ABI_IDENTITIES.partnershipBond)
      .toBe('0x954aeee609e92a621e726dcae77be193085651626941d582ca2f7f3b7c073d8c');
    expect(fixture.deployments.governance.functions).toHaveLength(45);
    expect(fixture.deployments.governance.functions.map(
      (method: { signature: string }) => method.signature,
    )).toEqual(expect.arrayContaining([
      'CANONICAL_POLICY_SHA256()',
      'PROPOSAL_LIFETIME()',
      'callPolicy(address,bytes4)',
      'canonicalRuntimeCodehash(address)',
      'constructorPinnedPolicyTarget(address)',
      'governancePolicyCommitment()',
      'isGovernanceCallAllowed(address,uint256,bytes)',
      'pinCanonicalAddress(address,bytes32)',
      'policyCommitment()',
      'policyVersion()',
      'proposalPolicyCommitment(uint256)',
      'setCallPolicy(address,bytes4,bool,uint96,bytes32)',
    ]));
    expect(fixture.deployments.governance.functions.map(
      (method: { signature: string }) => method.signature,
    )).not.toContain('setCallPolicy(address,bytes4,bool,uint96)');
    expect(v3.V3_ABI_IDENTITIES.governance)
      .toBe('0xcf81d69fed252f21bfd7661ff53868aa81b9ce1a4a1c89edbec9eaabffa7ad0a');
    expect(fixture.deployments.mandateRegistry.functions).toHaveLength(24);
    expect(fixture.deployments.mandateRegistry.functions.map(
      (method: { signature: string }) => method.signature,
    )).toEqual(expect.arrayContaining([
      'GOVERNANCE_POLICY_MODULE_ID()',
      'identityRegistry()',
      'identityRegistryCodehash()',
    ]));
    expect(v3.V3_ABI_IDENTITIES.mandateRegistry)
      .toBe('0xe31e9d196bf5a37c328313f688b0d6eff881847e6c46437c1fc617faff8e80e3');
    expect((v3.V3_ABI_IDENTITIES as Record<string, unknown>).taskEscrow).toBeUndefined();
  });

  it('derives the public supported set from the canonical support manifest', () => {
    const manifest = JSON.parse(readFileSync(join(
      root,
      '..',
      '..',
      'v3-migration',
      'completeness',
      'supported-v3-modules.json',
    ), 'utf8'));
    const supported = new Map(manifest.modules
      .filter((entry: { supportStatus: string }) => entry.supportStatus === 'supported-source')
      .map((entry: { module: string; path: string }) => [entry.module, entry.path]));
    for (const [deployment, metadata] of Object.entries(v3.V3_SUPPORTED_MODULES)) {
      expect(v3.V3_SUPPORTED_DEPLOYMENT_NAMES).toContain(deployment);
      expect(supported.get(metadata.solidityModule)).toBe(metadata.sourcePath);
    }
    expect(v3.V3_SUPPORTED_DEPLOYMENT_NAMES).not.toContain('taskEscrow');
    expect(supported.has('TaskEscrowV3')).toBe(false);
  });

  it('binds reviewed artifact snapshots to the current Solidity source content', () => {
    const sourceRoot = join(root, '..', '..', 'v3-src');
    for (const artifactName of [
      'AccountabilityBondV3Safe',
      'PartnershipBondV3Safe',
      'MultisigGovernanceV3',
      'MandateRegistryV3',
    ]) {
      const artifact = JSON.parse(readFileSync(join(
        root,
        'tests',
        'fixtures',
        'reviewed-artifacts',
        `${artifactName}.json`,
      ), 'utf8'));
      expect(artifact.compilerVersion).toBe('0.8.24');
      const metadata = JSON.parse(artifact.metadata);
      let matchedSources = 0;
      for (const [sourceName, source] of Object.entries(metadata.sources) as [
        string,
        { keccak256: string },
      ][]) {
        const sourcePath = join(sourceRoot, sourceName);
        if (!existsSync(sourcePath)) continue;
        matchedSources += 1;
        expect(ethers.keccak256(readFileSync(sourcePath))).toBe(source.keccak256);
      }
      expect(matchedSources).toBeGreaterThan(0);
    }
  });

  it('scans every packed V3 file and exposes the supported V3 subpath', () => {
    const manifest = JSON.parse(execFileSync('npm', [
      'pack', '--dry-run', '--json', '--ignore-scripts',
    ], { cwd: root, encoding: 'utf8' }))[0];
    const names = manifest.files.map((file: { path: string }) => file.path);
    expect(names).toContain('dist/v3/index.js');
    expect(names).toContain('dist/v3/index.d.ts');
    const packedV3 = names
      .filter((name: string) => name.startsWith('dist/v3/'))
      .map((name: string) => readFileSync(join(root, name), 'utf8'))
      .join('\n');
    expect(packedV3).not.toMatch(/wire to|TODO|FIXME/);
    const packageJson = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
    expect(packageJson.exports['./v3']).toEqual({
      types: './dist/v3/index.d.ts',
      import: './dist/esm/v3/index.js',
      require: './dist/v3/index.js',
      default: './dist/v3/index.js',
    });
    expect(packageJson.sideEffects).toBe(false);
  });

  it('loads both root and direct V3 ESM entry points', () => {
    const output = execFileSync(process.execPath, [
      '--input-type=module',
      '--eval',
      `const root = await import(${JSON.stringify(`file://${join(dist, 'esm', 'index.js')}`)});
       const direct = await import(${JSON.stringify(`file://${join(dist, 'esm', 'v3', 'index.js')}`)});
       process.stdout.write(JSON.stringify({
         hasV2: typeof root.VaultfireSDK === 'function',
         namespaced: root.v3.V3_PROTOCOL_VERSION,
         direct: direct.V3_PROTOCOL_VERSION
       }));`,
    ], { cwd: root, encoding: 'utf8' });
    expect(JSON.parse(output)).toEqual({
      hasV2: true,
      namespaced: '3.0.0',
      direct: '3.0.0',
    });
  });
});
