import { expect, test } from '@playwright/test';
import { getStackConfig, validateStackHealthy } from '../setup/stack';

const STACK_CONFIG = getStackConfig();
const STACK_RPC_URL = STACK_CONFIG.rpcUrl;

// Solidity function selectors
const GET_LIQUIDITY_PARAMS_SELECTOR = '0xbd53c0dc'; // getLiquidityParams()
const POSITIONS_SELECTOR = '0xf86aafc0'; // positions(uint8)

// Optimizer.sol invariants (capitalInefficiency + anchorShare = 1e18)
const ONE_ETHER = 10n ** 18n;

// Position stages
const STAGE_FLOOR = 0;
const STAGE_ANCHOR = 1;
const STAGE_DISCOVERY = 2;

// TICK_SPACING from ThreePositionStrategy
const TICK_SPACING = 200;

interface LiquidityParams {
  capitalInefficiency: bigint;
  anchorShare: bigint;
  anchorWidth: bigint;
  discoveryDepth: bigint;
}

async function rpcCall(method: string, params: unknown[]): Promise<unknown> {
  const response = await fetch(STACK_RPC_URL, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }),
  });
  const data = await response.json();
  if (data.error) throw new Error(`RPC error: ${data.error.message}`);
  return data.result;
}

async function readLiquidityParams(optimizerAddress: string): Promise<LiquidityParams> {
  const result = (await rpcCall('eth_call', [
    { to: optimizerAddress, data: GET_LIQUIDITY_PARAMS_SELECTOR },
    'latest',
  ])) as string;

  return {
    capitalInefficiency: BigInt('0x' + result.slice(2, 66)),
    anchorShare: BigInt('0x' + result.slice(66, 130)),
    anchorWidth: BigInt('0x' + result.slice(130, 194)),
    discoveryDepth: BigInt('0x' + result.slice(194, 258)),
  };
}

function toInt24(val: bigint): number {
  const n = Number(val & 0xffffffn);
  return n >= 0x800000 ? n - 0x1000000 : n;
}

test.describe('Optimizer Integration', () => {
  test.beforeAll(async () => {
    await validateStackHealthy(STACK_CONFIG);
  });

  test('Optimizer proxy returns valid parameters', async () => {
    const optimizerAddress = STACK_CONFIG.contracts.OptimizerProxy;
    if (!optimizerAddress) {
      console.log(
        '[TEST] SKIP: OptimizerProxy not in deployments-local.json (older deployment)',
      );
      test.skip();
      return;
    }

    console.log(`[TEST] OptimizerProxy: ${optimizerAddress}`);

    const params = await readLiquidityParams(optimizerAddress);
    console.log(`[TEST] capitalInefficiency: ${params.capitalInefficiency}`);
    console.log(`[TEST] anchorShare:         ${params.anchorShare}`);
    console.log(`[TEST] anchorWidth:         ${params.anchorWidth}`);
    console.log(`[TEST] discoveryDepth:      ${params.discoveryDepth}`);

    // Optimizer.sol invariants:
    // capitalInefficiency + anchorShare == 1e18
    expect(params.capitalInefficiency + params.anchorShare).toBe(ONE_ETHER);
    // discoveryDepth == anchorShare
    expect(params.discoveryDepth).toBe(params.anchorShare);
    // anchorWidth in [10, 80]
    expect(params.anchorWidth).toBeGreaterThanOrEqual(10n);
    expect(params.anchorWidth).toBeLessThanOrEqual(80n);

    console.log('[TEST] Optimizer returns valid parameters (invariants satisfied)');
  });

  test('bootstrap positions reflect valid optimizer anchorWidth', async () => {
    const lmAddress = STACK_CONFIG.contracts.LiquidityManager;
    const optimizerAddress = STACK_CONFIG.contracts.OptimizerProxy;
    if (!optimizerAddress) {
      test.skip();
      return;
    }

    // Read anchor position from LM (created by bootstrap's recenter call).
    // Note: optimizer state may have changed since bootstrap (e.g. staking activity in
    // earlier tests), so we don't read the *current* optimizer params here.  Instead
    // we reverse-calculate the anchorWidth that was in effect when recenter() ran and
    // verify it falls within Optimizer.sol's valid range [10, 80].
    const anchorResult = (await rpcCall('eth_call', [
      {
        to: lmAddress,
        data: `${POSITIONS_SELECTOR}${'1'.padStart(64, '0')}`, // Stage.ANCHOR = 1
      },
      'latest',
    ])) as string;

    const tickLower = toInt24(BigInt('0x' + anchorResult.slice(66, 130)));
    const tickUpper = toInt24(BigInt('0x' + anchorResult.slice(130, 194)));
    const anchorSpread = tickUpper - tickLower;

    console.log(`[TEST] Anchor position: ticks=[${tickLower}, ${tickUpper}], spread=${anchorSpread}`);

    // Reverse the formula to recover anchorWidth:
    // anchorSpread = 2 * (TICK_SPACING + (34 * anchorWidth * TICK_SPACING / 100))
    // => anchorWidth = (anchorSpread / 2 - TICK_SPACING) * 100 / (34 * TICK_SPACING)
    const halfSpread = anchorSpread / 2;
    expect(halfSpread).toBeGreaterThan(TICK_SPACING);

    const impliedAnchorWidth = Math.round(((halfSpread - TICK_SPACING) * 100) / (34 * TICK_SPACING));
    console.log(`[TEST] Implied anchorWidth from spread: ${impliedAnchorWidth}`);

    // Optimizer.sol constrains anchorWidth to [10, 80]
    expect(impliedAnchorWidth).toBeGreaterThanOrEqual(10);
    expect(impliedAnchorWidth).toBeLessThanOrEqual(80);

    // Confirm the implied anchorWidth reproduces the exact spread (no rounding error)
    const expectedSpacing = TICK_SPACING + (34 * impliedAnchorWidth * TICK_SPACING) / 100;
    const expectedSpread = expectedSpacing * 2;
    expect(anchorSpread).toBe(expectedSpread);

    console.log(`[TEST] Anchor spread ${anchorSpread} corresponds to valid anchorWidth=${impliedAnchorWidth}`);
  });

  test('all three positions have valid relative sizing', async () => {
    const lmAddress = STACK_CONFIG.contracts.LiquidityManager;
    const optimizerAddress = STACK_CONFIG.contracts.OptimizerProxy;
    if (!optimizerAddress) {
      test.skip();
      return;
    }

    // Read all three positions
    const positions = [];
    const stageNames = ['FLOOR', 'ANCHOR', 'DISCOVERY'];
    for (const stage of [STAGE_FLOOR, STAGE_ANCHOR, STAGE_DISCOVERY]) {
      const stageHex = stage.toString(16).padStart(64, '0');
      const result = (await rpcCall('eth_call', [
        { to: lmAddress, data: `${POSITIONS_SELECTOR}${stageHex}` },
        'latest',
      ])) as string;

      const liquidity = BigInt('0x' + result.slice(2, 66));
      const tickLower = toInt24(BigInt('0x' + result.slice(66, 130)));
      const tickUpper = toInt24(BigInt('0x' + result.slice(130, 194)));
      const spread = tickUpper - tickLower;

      positions.push({ liquidity, tickLower, tickUpper, spread });
      console.log(`[TEST] ${stageNames[stage]}: spread=${spread}, liquidity=${liquidity}`);
    }

    // Floor should be narrow (TICK_SPACING width = 200 ticks)
    expect(positions[0].spread).toBe(TICK_SPACING);
    console.log('[TEST] Floor has expected narrow width (200 ticks)');

    // Anchor should be wider than floor
    expect(positions[1].spread).toBeGreaterThan(positions[0].spread);
    console.log('[TEST] Anchor is wider than floor');

    // Discovery should have significant spread
    expect(positions[2].spread).toBeGreaterThan(0);
    console.log('[TEST] Discovery has positive spread');

    // Floor liquidity should be highest (concentrated in narrow range)
    expect(positions[0].liquidity).toBeGreaterThan(positions[1].liquidity);
    console.log('[TEST] Floor liquidity > anchor liquidity (as expected for concentrated position)');

    console.log('[TEST] All position sizing validated against optimizer parameters');
  });
});
