/**
 * Focused safety tests for /sign/v3-launch (V3 launch console).
 *
 * Dependency-free by design: runs with `node --test tests/v3-launch-console.test.mjs`
 * so it can be executed without installing node_modules. tests/v3-launch-console.test.js
 * mirrors these assertions for the Jest suite.
 *
 * Covers: wallet allowlisting, chain gating, fail-closed signing when the
 * transaction package/approval artifact are missing, truthful state labels,
 * absence of any private-key or seed-phrase field, absence of any
 * signing/sending/broadcast call, dataset-to-evidence drift, and retention of
 * the connected EIP-1193 provider.
 */
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import path from 'node:path';
import test from 'node:test';
import { fileURLToPath } from 'node:url';

import {
  AUTHORIZED_BASE_DEPLOYER,
  BASE_CHAIN_ID,
  REJECTED_V3_AUTHORITY_ADDRESSES,
  evaluateOperatorAccess,
  evaluateSigningAvailability,
  isAuthorizedDeployer,
  isBaseChain,
  isCanonicalWallet,
  normalizeAddress,
  resolveTrackState,
  screenPublicRoleInput,
} from '../app/sign/v3-launch/launchGuards.js';
import { buildData, serialize, OUTPUT_RELATIVE_PATH } from '../tools/build_v3_launch_console_data.mjs';

const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const read = (relative) => readFileSync(path.join(root, relative), 'utf8');

const consoleData = JSON.parse(read(OUTPUT_RELATIVE_PATH));
const pageSource = read('app/sign/v3-launch/V3LaunchConsole.tsx');
const routeSource = read('app/sign/v3-launch/page.tsx');
const guardSource = read('app/sign/v3-launch/launchGuards.js');
const walletContextSource = read('app/lib/WalletAuthContext.tsx');

/** Page source with comments removed, so prose in the file header cannot mask a real call. */
const pageCode = pageSource.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');

/* ---------------------------------------------------------------- allowlist */

test('only the evidenced Base deployer is authorized, case-insensitively', () => {
  assert.equal(isAuthorizedDeployer(AUTHORIZED_BASE_DEPLOYER), true);
  assert.equal(isAuthorizedDeployer(AUTHORIZED_BASE_DEPLOYER.toLowerCase()), true);
  assert.equal(isAuthorizedDeployer(AUTHORIZED_BASE_DEPLOYER.toUpperCase().replace('0X', '0x')), true);
  assert.equal(isAuthorizedDeployer(`  ${AUTHORIZED_BASE_DEPLOYER}  `), true);
});

test('every other wallet value fails closed', () => {
  for (const value of [
    null,
    undefined,
    '',
    '   ',
    0,
    {},
    [],
    'ghostkey.base.eth',
    '0x0000000000000000000000000000000000000000',
    '0xb7d84310847f5a2288CFE77638B184b0618827c5',
    AUTHORIZED_BASE_DEPLOYER.slice(0, -1),
    `${AUTHORIZED_BASE_DEPLOYER}00`,
  ]) {
    assert.equal(isAuthorizedDeployer(value), false, `must reject ${String(value)}`);
  }
  assert.equal(normalizeAddress('0xZZ'), null);
});

test('operator-rejected addresses can never be authorized', () => {
  assert.equal(REJECTED_V3_AUTHORITY_ADDRESSES.length, 2);
  const rejected = REJECTED_V3_AUTHORITY_ADDRESSES.map((e) => e.address);
  assert.ok(rejected.includes('0xA054f831B562e729F8D268291EBde1B2EDcFb84F'));
  assert.ok(rejected.includes('0x5a03C962227Dd478B5CD2B9f09E81105096154ad'));
  for (const address of rejected) {
    assert.equal(isAuthorizedDeployer(address), false);
    assert.equal(isAuthorizedDeployer(address.toLowerCase()), false);
    const access = evaluateOperatorAccess({ address, chainId: BASE_CHAIN_ID });
    assert.equal(access.unlocked, false);
    assert.equal(access.rejected, true);
    assert.match(access.blockers.join(' '), /permanently rejected for V3 authority/);
  }
});

test('the Solana V2 manifest payee is never treated as V3 authority', () => {
  assert.equal(consoleData.authority.solanaPublicPayee.classification, 'V2 manifest evidence only');
  assert.match(consoleData.authority.solanaPublicPayee.disposition, /not a V3 authority/);
  assert.equal(isAuthorizedDeployer(consoleData.authority.solanaPublicPayee.value), false);
});

test('canonical wallet surface is Base App / Coinbase, not MetaMask or WalletConnect', () => {
  assert.equal(isCanonicalWallet('base'), true);
  assert.equal(isCanonicalWallet('coinbase'), true);
  assert.equal(isCanonicalWallet('metamask'), false);
  assert.equal(isCanonicalWallet('walletconnect'), false);
  assert.match(consoleData.authority.base.canonicalConnectionPath.surface, /Base App/);
  assert.match(consoleData.authority.base.canonicalConnectionPath.signingModel, /wagmi\/viem/);
});

/* -------------------------------------------------------------- chain gating */

test('Base actions require chain 8453', () => {
  assert.equal(BASE_CHAIN_ID, 8453);
  for (const ok of [8453, '8453', '0x2105', '0X2105']) assert.equal(isBaseChain(ok), true, String(ok));
  for (const bad of [1, 10, 84532, 8454, null, undefined, '', 'base', '8453garbage', '0x2105garbage', NaN, {}, true]) {
    assert.equal(isBaseChain(bad), false, String(bad));
  }
});

test('authorized wallet on a non-Base chain stays locked with an explicit reason', () => {
  const access = evaluateOperatorAccess({ address: AUTHORIZED_BASE_DEPLOYER, chainId: 1 });
  assert.equal(access.authorized, true);
  assert.equal(access.onBaseChain, false);
  assert.equal(access.unlocked, false);
  assert.match(access.blockers.join(' '), /require chain id 8453/);
});

test('authorized wallet on Base unlocks the read-only view only', () => {
  const access = evaluateOperatorAccess({
    address: AUTHORIZED_BASE_DEPLOYER,
    chainId: BASE_CHAIN_ID,
    activeWallet: 'base',
  });
  assert.equal(access.unlocked, true);
  assert.deepEqual(access.blockers, []);
  const signing = evaluateSigningAvailability({
    access,
    transactionPackagePresent: consoleData.approvals.transactionPackagePresent,
    approvalArtifactPresent: consoleData.approvals.approvalArtifactPresent,
    approvalPhraseRequired: consoleData.approvals.baseReadOnlyPhrase,
  });
  assert.equal(signing.canSign, false);
  assert.equal(signing.controlsRendered, false);
});

/* ------------------------------------------------- fail-closed signing gates */

test('signing needs BOTH the transaction package and the approval artifact', () => {
  const access = { unlocked: true };
  const noPackage = evaluateSigningAvailability({
    access,
    transactionPackagePresent: false,
    approvalArtifactPresent: true,
  });
  assert.equal(noPackage.canSign, false);
  assert.match(noPackage.missing.join(' '), /verified immutable transaction package/);

  const noApproval = evaluateSigningAvailability({
    access,
    transactionPackagePresent: true,
    approvalArtifactPresent: false,
  });
  assert.equal(noApproval.canSign, false);
  assert.match(noApproval.missing.join(' '), /approval artifact/);

  const wrongPhrase = evaluateSigningAvailability({
    access,
    transactionPackagePresent: true,
    approvalArtifactPresent: true,
    approvalPhraseRequired: 'APPROVE BASE V3 READ-ONLY DEPLOYMENT',
    approvalPhraseProvided: 'approve base v3 read-only deployment',
  });
  assert.equal(wrongPhrase.canSign, false);
  assert.match(wrongPhrase.missing.join(' '), /exact approval phrase/);

  const unauthenticated = evaluateSigningAvailability({
    access: { unlocked: false },
    transactionPackagePresent: true,
    approvalArtifactPresent: true,
  });
  assert.equal(unauthenticated.canSign, false);
});

test('the shipped dataset asserts neither release artifact exists', () => {
  assert.equal(consoleData.approvals.transactionPackagePresent, false);
  assert.equal(consoleData.approvals.approvalArtifactPresent, false);
  assert.equal(consoleData.approvals.baseReadOnlyPhrase, 'APPROVE BASE V3 READ-ONLY DEPLOYMENT');
  assert.equal(consoleData.approvals.partnerPilotPhrase, 'APPROVE CAPPED BASE V3 PARTNER PILOT');
});

test('the console renders no signing, sending, broadcast, payment, or activation call', () => {
  for (const forbidden of [
    'eth_sendTransaction',
    'eth_sign',
    'personal_sign',
    'signTypedData',
    'wallet_sendCalls',
    'sendTransaction',
    'createPaymentHeader',
    'createWalletClient',
    'deployContract',
    '/api/x402',
    'bazaar',
    'fetch(',
    'axios',
  ]) {
    assert.ok(
      !pageCode.includes(forbidden),
      `V3LaunchConsole.tsx must not reference ${forbidden}`,
    );
  }
  // The only wallet RPC the console performs is a chain switch plus reads.
  const rpcMethods = [...pageCode.matchAll(/'(eth_[a-zA-Z]+|wallet_[a-zA-Z]+)'/g)].map((m) => m[1]);
  assert.deepEqual(rpcMethods, []);
  assert.ok(pageCode.includes('requestChainSwitch(BASE_CHAIN_ID_HEX)'));
});

/* -------------------------------------------------------- truthful labelling */

test('dataset tracks are exactly the four required tracks and none claims deployment', () => {
  assert.deepEqual(
    consoleData.tracks.map((t) => t.id),
    ['base-read-only-bootstrap', 'base-capped-partner-pilot', 'solana-v3', 'x402-v3'],
  );
  for (const track of consoleData.tracks) {
    assert.equal(track.deployedAddresses, null, track.id);
    assert.equal(track.deploymentTransactions, null, track.id);
    assert.ok(['prepared', 'blocked'].includes(resolveTrackState(track)), track.id);
    assert.ok(track.missingArtifacts.length > 0, track.id);
  }
  const byId = Object.fromEntries(consoleData.tracks.map((t) => [t.id, t]));
  assert.equal(resolveTrackState(byId['base-read-only-bootstrap']), 'prepared');
  assert.equal(resolveTrackState(byId['base-capped-partner-pilot']), 'blocked');
  assert.equal(resolveTrackState(byId['solana-v3']), 'blocked');
  assert.equal(resolveTrackState(byId['x402-v3']), 'blocked');
});

test('state resolver refuses to call anything deployed or activated without evidence', () => {
  assert.equal(resolveTrackState({ state: 'prepared', flags: { broadcast: true } }), 'prepared');
  assert.equal(
    resolveTrackState({ state: 'prepared', flags: { broadcast: true }, deployedAddresses: ['0x1'] }),
    'prepared',
  );
  assert.equal(
    resolveTrackState({
      state: 'prepared',
      flags: { broadcast: true },
      deployedAddresses: ['0x1'],
      deploymentTransactions: ['0xhash'],
    }),
    'deployed',
  );
  assert.equal(
    resolveTrackState({
      state: 'prepared',
      flags: { activated: true },
      deployedAddresses: ['0x1'],
      deploymentTransactions: ['0xhash'],
    }),
    'activated',
  );
});

test('recorded V3 deployment addresses are all null and no transaction hash exists', () => {
  const addresses = consoleData.verifiedDeployments.baseV3Addresses;
  assert.ok(Object.keys(addresses).length > 0);
  for (const [name, value] of Object.entries(addresses)) {
    assert.equal(value, null, `${name} must remain null`);
  }
  assert.deepEqual(consoleData.verifiedDeployments.transactionHashes, []);
  assert.match(consoleData.verifiedDeployments.baseV3AddressStatus, /none recorded/);
});

test('x402 stays opt-in with V2 default, and Solana stays not deployed', () => {
  const byId = Object.fromEntries(consoleData.tracks.map((t) => [t.id, t]));
  const x402 = byId['x402-v3'];
  assert.equal(x402.flags.v2Default, true);
  assert.equal(x402.flags.v3LiveSettlementVerified, false);
  assert.equal(x402.flags.routeActivationOffered, false);
  assert.equal(x402.flags.paymentOffered, false);
  assert.equal(x402.flags.bazaarMutationOffered, false);
  assert.match(x402.stateLabel, /configured-unverified/);

  const solana = byId['solana-v3'];
  assert.equal(solana.flags.deployed, false);
  assert.equal(solana.flags.signingOffered, false);
  assert.ok(solana.facts.some((f) => f.value === 'not_deployed'));
  assert.match(solana.stateLabel, /not deployed/);
});

test('page copy carries the truthful state labels from the dataset', () => {
  assert.ok(pageSource.includes('read-only and locked'));
  assert.ok(pageSource.includes('resolveTrackState'));
  assert.ok(pageSource.includes('Deployed addresses recorded: none'));
  assert.ok(routeSource.includes('robots: { index: false, follow: false }'));
  for (const track of consoleData.tracks) {
    assert.ok(['prepared', 'blocked', 'deployed', 'activated'].includes(track.state), track.id);
  }
});

/* ------------------------------------------------------ no signing material */

test('the console has no private key, seed phrase, or keystore field', () => {
  for (const forbidden of [
    'type="password"',
    "type='password'",
    'privateKey',
    'seedPhrase',
    'unlock(',
  ]) {
    assert.ok(!pageCode.includes(forbidden), `V3LaunchConsole.tsx must not contain ${forbidden}`);
  }
  // The only mentions of signing material are in the warning copy, never as a field.
  for (const word of ['private key', 'seed phrase', 'mnemonic', 'keystore']) {
    const hits = pageCode.toLowerCase().split(word).length - 1;
    assert.ok(hits <= 2, `${word} appears ${hits} times in code`);
  }
  const inputNames = [...pageCode.matchAll(/name="([^"]+)"/g)].map((m) => m[1]);
  assert.deepEqual(inputNames, ['publicAddressFormatCheck']);
  const inputCount = (pageCode.match(/<input/g) || []).length;
  assert.equal(inputCount, 1);
  assert.equal((pageSource.match(/<textarea/g) || []).length, 0);
  assert.ok(pageSource.includes('cannot accept a private key, seed phrase'));
});

test('role input screening rejects anything that looks like signing material', () => {
  assert.equal(screenPublicRoleInput(AUTHORIZED_BASE_DEPLOYER).safe, true);
  assert.equal(screenPublicRoleInput('').safe, true);
  const seed = 'legal winner thank year wave sausage worth useful legal winner thank yellow';
  assert.equal(screenPublicRoleInput(seed).safe, false);
  assert.match(screenPublicRoleInput(seed).reason, /seed phrase/);
  const secret = `0x${'a'.repeat(64)}`;
  assert.equal(screenPublicRoleInput(secret).safe, false);
  assert.equal(screenPublicRoleInput('my private key is 0x1').safe, false);
  assert.equal(screenPublicRoleInput('not-an-address').safe, false);
});

test('role inputs are requested without inventing any value', () => {
  const roles = consoleData.roleInputs;
  assert.equal(roles.requiredCounts.governanceSignerAddresses, 5);
  assert.equal(roles.requiredCounts.guardianAddresses, 1);
  assert.equal(roles.requiredCounts.treasuryAddresses, 1);
  assert.equal(roles.requiredCounts.signerCustodyDisclosures, 5);
  assert.equal(roles.governance.signerCount, 5);
  assert.deepEqual(roles.governance.signerSlots, [1, 2, 3, 4, 5]);
  for (const signer of roles.emptyTemplate.governance.signers) {
    assert.equal(signer.address, null);
    assert.equal(signer.custody.classification, null);
  }
  assert.equal(roles.emptyTemplate.guardian.address, null);
  assert.equal(roles.emptyTemplate.treasury.address, null);
  assert.equal(roles.emptyTemplate.authorization.bootstrapDeploymentAuthorized, false);
  assert.ok(Object.keys(roles.allowedCustodyClassifications).length >= 2);
  assert.ok(roles.confirmations.includes('noPrivateKeySeedPhraseOrSigningMaterialIsIncluded'));
});

/* ----------------------------------------------------- evidence drift + data */

test('checked-in console data matches its evidence sources byte for byte', () => {
  assert.equal(read(OUTPUT_RELATIVE_PATH), serialize(buildData()));
});

test('provenance records source hashes and makes no readiness claim', () => {
  assert.equal(consoleData.provenance.sources.length, 4);
  for (const source of consoleData.provenance.sources) {
    assert.match(source.sha256, /^[0-9a-f]{64}$/);
    assert.ok(source.bytes > 0);
  }
  assert.equal(consoleData.provenance.independentHumanAuditComplete, false);
  assert.match(consoleData.provenance.auditClaim, /No bug-free, audited, production-ready/);
  for (const claim of ['deployment', 'signing', 'broadcast', 'settlement']) {
    assert.ok(consoleData.provenance.claimsNotMade.includes(claim), claim);
  }
});

test('later, pinned current evidence supersedes only the two obsolete historical findings', () => {
  const byId = Object.fromEntries(consoleData.tracks.map((track) => [track.id, track]));
  const baseBlockers = Object.fromEntries(byId['base-read-only-bootstrap'].blockers.map((blocker) => [blocker.id, blocker]));
  const x402Blockers = Object.fromEntries(byId['x402-v3'].blockers.map((blocker) => [blocker.id, blocker]));

  assert.equal(baseBlockers['V2-ABI-001'], undefined);
  assert.equal(x402Blockers['X402-VALID-AFTER'], undefined);
  assert.equal(
    byId['x402-v3'].facts.find((fact) => fact.label === 'Signed validAfter bound and tested')?.value,
    'true',
  );
  assert.equal(
    byId['x402-v3'].missingArtifacts.some((artifact) => artifact.includes('validAfter')),
    false,
  );

  for (const id of ['X402-OWNER-EVIDENCE-001', 'X402-DEPLOYMENT-IDENTITY', 'X402-BASE-SETTLEMENT', 'X402-SOLANA-SETTLEMENT', 'X402-DURABLE-CONCURRENCY']) {
    assert.notEqual(x402Blockers[id].provenance.basis, 'current-status-correction', id);
  }
  assert.deepEqual(consoleData.provenance.currentStatusCorrections.correctedFindingIds, ['V2-ABI-001', 'X402-VALID-AFTER']);
  for (const claim of ['audit completion', 'deployment readiness', 'pilot readiness', 'payment readiness']) {
    assert.ok(consoleData.provenance.currentStatusCorrections.claimsNotMade.includes(claim), claim);
  }
});

/* --------------------------------------------- retained EIP-1193 provider */

test('wallet context retains the connected provider instead of guessing window.ethereum', () => {
  assert.ok(walletContextSource.includes("source = 'coinbase'"));
  assert.ok(walletContextSource.includes("source = 'walletconnect'"));
  assert.ok(walletContextSource.includes('retainProvider(wp as Eip1193Provider, source)'));
  assert.ok(walletContextSource.includes('providerRef.current'));
  assert.ok(walletContextSource.includes('requestChainSwitch'));
  // The chain switch must route through the retained provider only.
  const switchBlock = walletContextSource.slice(
    walletContextSource.indexOf('const requestChainSwitch'),
    walletContextSource.indexOf('const unlock'),
  );
  assert.ok(switchBlock.includes('providerRef.current'));
  assert.ok(!switchBlock.includes('window.ethereum'));
  assert.match(switchBlock, /providerRef\.current !== wp \|\| sessionEpochRef\.current !== sessionEpoch/);
  assert.match(switchBlock, /Wallet did not switch to chain/);
  // Stale providers must neither keep duplicate listeners nor overwrite the
  // current account/chain after another wallet has been selected.
  assert.ok(walletContextSource.includes('const unsubscribeProvider'));
  assert.ok(walletContextSource.includes('if (providerRef.current !== wp) return;'));
  assert.ok(walletContextSource.includes('sessionEpochRef.current !== connectionEpoch'));
  // A legacy private-key unlock has no provider provenance and must clear any
  // previously retained provider and chain instead of inheriting its authority.
  const unlockBlock = walletContextSource.slice(
    walletContextSource.lastIndexOf('const unlock'),
    walletContextSource.indexOf('return (', walletContextSource.lastIndexOf('const unlock')),
  );
  assert.ok(unlockBlock.includes('unsubscribeProvider();'));
  assert.ok(unlockBlock.includes('retainProvider(null, null);'));
  assert.ok(unlockBlock.includes('setChainId(null)'));
  // Backward compatibility: the pre-existing public API is still exported.
  for (const field of ['isUnlocked', 'address', 'chainId', 'isRestoring', 'activeWallet', 'connectWallet', 'unlock']) {
    assert.ok(walletContextSource.includes(field), field);
  }
});

test('the console never reaches for window.ethereum or a raw wallet provider', () => {
  assert.ok(!pageCode.includes('window.ethereum'));
  assert.ok(!pageCode.includes('ethereum'));
  assert.ok(pageCode.includes('providerSource'));
  // Solana detection is presence-only: no connect/request/sign call.
  assert.ok(pageCode.includes('window as unknown as { solana?'));
  assert.ok(!pageCode.includes('solana.connect'));
  assert.ok(!pageCode.includes('signTransaction'));
});

test('guard module hardcodes no secret and only public constants', () => {
  assert.ok(guardSource.includes(AUTHORIZED_BASE_DEPLOYER));
  assert.ok(!/0x[0-9a-fA-F]{64}/.test(guardSource.replace(/\{64\}/g, '')));
  assert.ok(!guardSource.includes('process.env'));
});
