// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.19;

/**
 * @title VWAPFloorProtectionTest
 * @notice Regression tests for issue #543: VWAP must not inflate during buy-only recenter cycles.
 *
 * Root cause (pre-fix): shouldRecordVWAP was true on BUY events (ETH inflow).  An adversary
 * running N buy-recenter cycles continuously updated VWAP upward toward the current (inflated)
 * price.  When VWAP ≈ currentTick the mirrorTick formula placed the floor near the current
 * price, crystallising IL when the adversary finally sold all KRK.
 *
 * Fix: shouldRecordVWAP is now true only on SELL events (price falling / ETH outflow).
 * Buy-only cycles leave VWAP frozen at the historical bootstrap level, keeping the floor
 * conservatively anchored to that baseline.
 */

import { LiquidityManager } from "../src/LiquidityManager.sol";
import { ThreePositionStrategy } from "../src/abstracts/ThreePositionStrategy.sol";
import { TestEnvironment } from "./helpers/TestBase.sol";
import { UniSwapHelper } from "./helpers/UniswapTestBase.sol";

contract VWAPFloorProtectionTest is UniSwapHelper {
    address constant RECENTER_CALLER = address(0x7777);

    LiquidityManager lm;
    TestEnvironment testEnv;
    address feeDestination = makeAddr("fees");

    // How much ETH to give the LM and the attacker
    uint256 constant LM_ETH = 100 ether;
    uint256 constant ATTACKER_ETH = 2000 ether;

    function setUp() public {
        testEnv = new TestEnvironment(feeDestination);
        (, pool, weth, harberg,, lm,, token0isWeth) = testEnv.setupEnvironment(false);

        vm.deal(address(lm), LM_ETH);

        // Fund the swap account used by UniSwapHelper
        vm.deal(account, ATTACKER_ETH);
        vm.prank(account);
        weth.deposit{ value: ATTACKER_ETH / 2 }();
    }

    // =========================================================================
    // Core regression: VWAP must not inflate during buy-only cycles (#543)
    // =========================================================================

    /**
     * @notice VWAP stays at its bootstrap value throughout a buy-only attack sequence.
     *
     * Sequence:
     *   1. First recenter → creates positions (no fees yet, VWAP not recorded).
     *   2. Buy KRK (price rises) → second recenter → bootstraps VWAP (cumulativeVolume == 0).
     *   3. Repeat buy + recenter several more times.
     *   4. Assert getVWAP() is unchanged from the bootstrap recording.
     *
     * Before the fix this test would fail because every successful buy-direction recenter
     * updated VWAP with the new (higher) anchor price, pulling VWAP toward currentTick.
     */
    function test_vwapNotInflatedByBuyOnlyAttack() public {
        // ---- step 1: initial recenter sets up positions ----
        vm.prank(RECENTER_CALLER);
        lm.recenter();

        assertEq(lm.cumulativeVolume(), 0, "no fees collected yet: cumulativeVolume == 0");

        // ---- step 2: first buy + recenter → bootstrap ----
        buyRaw(25 ether); // push price up enough to satisfy amplitude check
        vm.warp(block.timestamp + 61); // TWAP catches up to post-buy price; cooldown passes

        vm.prank(RECENTER_CALLER);
        lm.recenter(); // cumulativeVolume == 0 → shouldRecordVWAP = true (bootstrap path)

        uint256 bootstrapVWAP = lm.getVWAP();
        assertGt(bootstrapVWAP, 0, "VWAP must be recorded at bootstrap");

        // ---- step 3: continued buy-only cycles ----
        uint256 successfulBuyCycles;
        uint256 ts = block.timestamp; // track explicitly to avoid Forge block.timestamp reset
        for (uint256 i = 0; i < 10; i++) {
            buyRaw(25 ether);
            ts += 61; // TWAP catches up; cooldown passes
            vm.warp(ts);
            vm.prank(RECENTER_CALLER);
            // Recenter may fail if amplitude isn't reached; that's fine.
            try lm.recenter() {
                successfulBuyCycles++;
            } catch { }
        }

        // Ensure at least some cycles succeeded so the test is meaningful.
        assertGt(successfulBuyCycles, 0, "at least one buy-recenter cycle must succeed");

        // ---- step 4: VWAP must be unchanged ----
        uint256 vwapAfterAttack = lm.getVWAP();
        assertEq(vwapAfterAttack, bootstrapVWAP, "VWAP must remain frozen at bootstrap value during buy-only cycles");
    }

    /**
     * @notice The floor is anchored conservatively (far from the inflated current price)
     *         after a buy-only attack, making extraction unprofitable.
     *
     * After N buy cycles the current tick is far above the bootstrap.  With VWAP frozen at
     * bootstrap, mirrorTick ≈ vwapTick ≈ bootstrapTick — much further from currentTick than
     * if VWAP had tracked upward.  The floor therefore sits near the original distribution
     * price, not the inflated price.
     */
    function test_floorConservativeAfterBuyOnlyAttack() public {
        vm.prank(RECENTER_CALLER);
        lm.recenter();

        // Bootstrap via first buy-recenter
        buyRaw(25 ether);
        vm.warp(block.timestamp + 61); // TWAP catches up; cooldown passes
        vm.prank(RECENTER_CALLER);
        lm.recenter();

        // Run several buy cycles
        uint256 ts = block.timestamp; // track explicitly to avoid Forge block.timestamp reset
        for (uint256 i = 0; i < 6; i++) {
            buyRaw(25 ether);
            ts += 61; // TWAP catches up; cooldown passes
            vm.warp(ts);
            vm.prank(RECENTER_CALLER);
            try lm.recenter() { } catch { }
        }

        // This test is written for token0isWeth=false (set in setUp via setupEnvironment(false,...)).
        // For !token0isWeth: buying KRK pushes tick UP (WETH/KRK price rises), so floor must be
        // BELOW (lower tick than) the inflated current tick.
        assertFalse(token0isWeth, "test assumes token0isWeth=false; update gap logic if changed");

        // Read floor and current tick
        (, int24 currentTick,,,,,) = pool.slot0();
        (, int24 floorTickLower, int24 floorTickUpper) = lm.positions(ThreePositionStrategy.Stage.FLOOR);

        int24 floorCenter = floorTickLower + (floorTickUpper - floorTickLower) / 2;

        // The floor must be BELOW the current inflated tick by a substantial margin —
        // at minimum the anchor spacing plus some additional buffer from VWAP anchoring.
        // We assert that the gap is at least 400 ticks (two tick spacings).
        int24 gap = currentTick - floorCenter;
        assertGt(gap, 400, "floor must be substantially below the inflated current tick");
    }

    // =========================================================================
    // Positive: VWAP bootstrap still works
    // =========================================================================

    /**
     * @notice The very first fee event always records VWAP regardless of direction.
     *
     * cumulativeVolume == 0 triggers unconditional recording to avoid the
     * vwapX96 == 0 fallback path.  This test confirms that path is preserved.
     */
    function test_vwapBootstrapsOnFirstFeeEvent() public {
        vm.prank(RECENTER_CALLER);
        lm.recenter();

        assertEq(lm.cumulativeVolume(), 0, "no VWAP data before first fees");

        buyRaw(25 ether);
        vm.warp(block.timestamp + 61); // TWAP catches up to post-buy price; cooldown passes

        vm.prank(RECENTER_CALLER);
        lm.recenter();

        assertGt(lm.cumulativeVolume(), 0, "bootstrap: cumulativeVolume must be positive");
        assertGt(lm.getVWAP(), 0, "bootstrap: VWAP must be positive after first fees");
    }

    // =========================================================================
    // Positive: VWAP updates when price falls (sell-side events)
    // =========================================================================

    /**
     * @notice VWAP can still be updated when price falls between recenters.
     *
     * Sequence:
     *   1. Buy large → recenter (bootstrap VWAP at high price).
     *   2. Sell all KRK → price falls below bootstrap.
     *   3. Recenter with price-fall direction → shouldRecordVWAP = true.
     *   4. If ETH fees were collected (buys happened in the prior cycle), VWAP updates.
     *      We verify at minimum that the recenter succeeds without reverting — i.e.
     *      the sell-direction path doesn't break the system.
     */
    function test_recenterSucceedsOnSellDirectionWithoutReverts() public {
        // Bootstrap
        vm.prank(RECENTER_CALLER);
        lm.recenter();

        uint256 ts = block.timestamp; // track explicitly to avoid Forge block.timestamp reset
        buyRaw(25 ether);
        ts += 61; // TWAP catches up to post-buy price; cooldown passes
        vm.warp(ts);
        vm.prank(RECENTER_CALLER);
        lm.recenter();

        // Sell back: harberg balance of `account` from the prior buy
        uint256 harbBalance = harberg.balanceOf(account);
        if (harbBalance > 0) {
            sellRaw(harbBalance);
        }

        // Recenter with price now lower (sell direction) — must not revert
        ts += 61; // TWAP catches up to post-sell price; cooldown passes
        vm.warp(ts);
        vm.prank(RECENTER_CALLER);
        try lm.recenter() {
        // success — sell-direction recenter works
        }
        catch (bytes memory reason) {
            // Amplitude not met is the only acceptable failure
            assertEq(
                keccak256(reason), keccak256(abi.encodeWithSignature("Error(string)", "amplitude not reached.")), "unexpected revert in sell-direction recenter"
            );
        }
    }

    // =========================================================================
    // Deployment bootstrap: seed trade seeds VWAP before protocol goes live
    // =========================================================================

    /**
     * @notice Verifies the deployment bootstrap sequence from DeployLocal.sol / DeployBase.sol.
     *
     * The deploy scripts execute:
     *   1. First recenter  — places bootstrap positions; no fees, cumulativeVolume stays 0.
     *   2. Seed buy        — small swap generates a non-zero WETH fee in the anchor position
     *                        and moves the tick >400 (amplitude gate for the second recenter).
     *   3. Second recenter — cumulativeVolume==0 path fires (shouldRecordVWAP=true) and
     *                        ethFee>0, so _recordVolumeAndPrice is called.
     *
     * After step 3, cumulativeVolume>0 and the bootstrap path is permanently closed to
     * external users.  This test mirrors that sequence and asserts the invariant holds.
     */
    function test_vwapBootstrappedBySeedTrade() public {
        // Step 1: Initial recenter — places positions, no fees yet.
        vm.prank(RECENTER_CALLER);
        lm.recenter();
        assertEq(lm.cumulativeVolume(), 0, "no fees before seed trade: cumulativeVolume must be 0");

        // Step 2: Seed buy — enough to move the tick >400 (amplitude gate) and generate fee.
        // 25 ether against a 100 ETH LM pool reliably satisfies the amplitude check
        // (same amount used across other bootstrap tests in this file).
        buyRaw(25 ether);
        vm.warp(block.timestamp + 61); // TWAP catches up to post-buy price; cooldown passes

        // Step 3: Second recenter — bootstrap path records VWAP.
        vm.prank(RECENTER_CALLER);
        lm.recenter();

        assertGt(lm.cumulativeVolume(), 0, "seed trade must bootstrap cumulativeVolume to non-zero");
        assertGt(lm.getVWAP(), 0, "seed trade must anchor VWAP to the real launch price");
    }

    // =========================================================================
    // Issue #609: tick-0 edge case — _hasRecenterTick guard
    // =========================================================================

    /**
     * @notice Verifies that the _hasRecenterTick guard prevents the direction
     *         comparison from running against the Solidity-default lastRecenterTick==0
     *         before any recenter has established a valid reference tick.
     *
     * Before fix #609, the bootstrap condition was `cumulativeVolume == 0`.
     * Both conditions produce identical behaviour in practice (lastRecenterTick is
     * always set before cumulativeVolume can become positive), but the explicit
     * _hasRecenterTick flag makes the intent clear and prevents regressions if the
     * volume-recording path is ever refactored.
     *
     * This test mirrors the deployment bootstrap sequence and then runs buy-only
     * cycles, asserting that VWAP remains frozen throughout — confirming that the
     * direction check (which now only fires after _hasRecenterTick is true) still
     * correctly blocks VWAP inflation during buy-only attacks.
     */
    function test_hasRecenterTickGuardPreventsTick0Ambiguity() public {
        // Before any recenter: lastRecenterTick == 0 (Solidity default).
        // The _hasRecenterTick flag must be false, so the bootstrap path fires
        // on the first recenter regardless of the default-0 comparison.
        assertEq(lm.lastRecenterTick(), 0, "lastRecenterTick defaults to 0 before first recenter");

        // ---- step 1: first recenter — positions deployed, no fees ----
        vm.prank(RECENTER_CALLER);
        lm.recenter();

        // lastRecenterTick is now set to the actual pool tick (far from 0).
        // _hasRecenterTick is true after this call.
        int24 tickAfterFirstRecenter = lm.lastRecenterTick();
        assertTrue(tickAfterFirstRecenter != 0, "pool tick should not be exactly 0 after init");

        // ---- step 2: buy + recenter → bootstrap VWAP ----
        buyRaw(25 ether);
        vm.warp(block.timestamp + 61);

        vm.prank(RECENTER_CALLER);
        lm.recenter();

        uint256 bootstrapVWAP = lm.getVWAP();
        assertGt(bootstrapVWAP, 0, "VWAP must be bootstrapped");
        assertGt(lm.cumulativeVolume(), 0, "cumulativeVolume must be positive after bootstrap");

        // ---- step 3: buy-only attack cycles ----
        uint256 successfulBuyCycles;
        uint256 ts = block.timestamp;
        for (uint256 i = 0; i < 8; i++) {
            buyRaw(25 ether);
            ts += 61;
            vm.warp(ts);
            vm.prank(RECENTER_CALLER);
            try lm.recenter() {
                successfulBuyCycles++;
            } catch { }
        }

        assertGt(successfulBuyCycles, 0, "at least one buy-recenter cycle must succeed");

        // ---- step 4: VWAP must remain frozen ----
        assertEq(lm.getVWAP(), bootstrapVWAP, "issue #609: VWAP must stay frozen during buy-only cycles (direction check + _hasRecenterTick guard)");
    }

    /**
     * @notice After a sell-then-buy sequence where lastRecenterTick could land
     *         near zero, buy-only cycles must still not inflate VWAP.
     *
     * This test exercises the direction comparison in the else-branch after
     * multiple recenters have occurred, verifying correctness regardless of
     * the specific lastRecenterTick value.
     */
    function test_vwapFrozenDuringBuyOnlyAfterSellRecenter() public {
        // Bootstrap
        vm.prank(RECENTER_CALLER);
        lm.recenter();

        buyRaw(25 ether);
        vm.warp(block.timestamp + 61);
        vm.prank(RECENTER_CALLER);
        lm.recenter();

        uint256 vwapAfterBootstrap = lm.getVWAP();
        assertGt(vwapAfterBootstrap, 0, "VWAP bootstrapped");

        // Sell back to move price down — triggers VWAP recording (sell direction)
        uint256 harbBal = harberg.balanceOf(account);
        if (harbBal > 0) {
            sellRaw(harbBal);
        }
        vm.warp(block.timestamp + 61);
        vm.prank(RECENTER_CALLER);
        try lm.recenter() { } catch { }

        // Snapshot VWAP after sell-direction update
        uint256 vwapAfterSell = lm.getVWAP();

        // Now run buy-only cycles — VWAP must not increase
        uint256 ts = block.timestamp;
        uint256 successfulBuyCycles;
        for (uint256 i = 0; i < 6; i++) {
            buyRaw(25 ether);
            ts += 61;
            vm.warp(ts);
            vm.prank(RECENTER_CALLER);
            try lm.recenter() {
                successfulBuyCycles++;
            } catch { }
        }

        if (successfulBuyCycles > 0) {
            assertEq(lm.getVWAP(), vwapAfterSell, "VWAP must stay frozen during buy-only cycles after sell-direction recenter");
        }
    }

    // =========================================================================
    // getLiquidityManager override for UniSwapHelper boundary helpers
    // =========================================================================

    function getLiquidityManager() external view override returns (ThreePositionStrategy) {
        return ThreePositionStrategy(address(lm));
    }
}
