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

import {OptimizerV3Push3} from "../src/OptimizerV3Push3.sol";
import {OptimizerInput} from "../src/IOptimizer.sol";
import "forge-std/Test.sol";

/**
 * @title OptimizerV3Push3Test
 * @notice Verifies that the transpiled Push3 optimizer produces correct
 *         bear/bull parameters via the 8-slot dyadic rational interface.
 *
 * Bear output: CI=0, AS=0.3e18, AW=100, DD=0.3e18
 * Bull output: CI=0, AS=1e18,   AW=20,  DD=1e18
 *
 * Bull condition: stakedPct > 91 AND penalty < 50
 *   where penalty = deltaS^3 * effIdx / 20
 */
contract OptimizerV3Push3Test is Test {
    OptimizerV3Push3 push3;

    uint256[30] TAX_RATES = [
        uint256(1),
        3,
        5,
        8,
        12,
        18,
        24,
        30,
        40,
        50,
        60,
        80,
        100,
        130,
        180,
        250,
        320,
        420,
        540,
        700,
        920,
        1200,
        1600,
        2000,
        2600,
        3400,
        4400,
        5700,
        7500,
        9700
    ];
    uint256 constant MAX_TAX = 9700;

    // Expected bear/bull outputs
    uint256 constant BEAR_CI = 0;
    uint256 constant BEAR_ANCHOR_SHARE = 3e17; // 0.3e18
    uint24 constant BEAR_ANCHOR_WIDTH = 100;
    uint256 constant BEAR_DISCOVERY = 3e17; // 0.3e18

    uint256 constant BULL_CI = 0;
    uint256 constant BULL_ANCHOR_SHARE = 1e18;
    uint24 constant BULL_ANCHOR_WIDTH = 20;
    uint256 constant BULL_DISCOVERY = 1e18;

    function setUp() public {
        push3 = new OptimizerV3Push3();
    }

    // ---- Helpers ----

    function _norm(uint256 taxIdx) internal view returns (uint256) {
        return TAX_RATES[taxIdx] * 1e18 / MAX_TAX;
    }

    function _pct(uint256 pct) internal pure returns (uint256) {
        return pct * 1e18 / 100;
    }

    /// @dev Build an 8-slot input array with only slots 0 and 1 populated.
    function _inputs(uint256 percentageStaked, uint256 averageTaxRate)
        internal
        pure
        returns (OptimizerInput[8] memory inp)
    {
        inp[0] = OptimizerInput({mantissa: int256(percentageStaked), shift: 0});
        inp[1] = OptimizerInput({mantissa: int256(averageTaxRate), shift: 0});
        // slots 2-7 default to (0, 0)
    }

    function _assertBear(uint256 ci, uint256 as_, uint24 aw, uint256 dd) internal pure {
        assertEq(ci, BEAR_CI, "bear: ci");
        assertEq(as_, BEAR_ANCHOR_SHARE, "bear: anchorShare");
        assertEq(aw, BEAR_ANCHOR_WIDTH, "bear: anchorWidth");
        assertEq(dd, BEAR_DISCOVERY, "bear: discoveryDepth");
    }

    function _assertBull(uint256 ci, uint256 as_, uint24 aw, uint256 dd) internal pure {
        assertEq(ci, BULL_CI, "bull: ci");
        assertEq(as_, BULL_ANCHOR_SHARE, "bull: anchorShare");
        assertEq(aw, BULL_ANCHOR_WIDTH, "bull: anchorWidth");
        assertEq(dd, BULL_DISCOVERY, "bull: discoveryDepth");
    }

    // ---- getLiquidityParams() stub guard ----

    function testGetLiquidityParamsReverts() public {
        vm.expectRevert("OptimizerV3Push3: not for production use");
        push3.getLiquidityParams();
    }

    // ---- Bear cases ----

    function testAlwaysBearAt0Percent() public view {
        for (uint256 t = 0; t < 30; t++) {
            (uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.calculateParams(_inputs(0, _norm(t)));
            _assertBear(ci, as_, aw, dd);
        }
    }

    function testAlwaysBearAt91Percent() public view {
        for (uint256 t = 0; t < 30; t++) {
            (uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.calculateParams(_inputs(_pct(91), _norm(t)));
            _assertBear(ci, as_, aw, dd);
        }
    }

    // ---- Bull cases ----

    function testBoundary92PercentLowestTax() public view {
        // deltaS=8, effIdx=0 → penalty=0 < 50 → BULL
        (uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.calculateParams(_inputs(_pct(92), _norm(0)));
        _assertBull(ci, as_, aw, dd);
    }

    function testBoundary92PercentTaxIdx1() public view {
        // deltaS=8, effIdx=1 → penalty=512*1/20=25 < 50 → BULL
        (uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.calculateParams(_inputs(_pct(92), _norm(1)));
        _assertBull(ci, as_, aw, dd);
    }

    function testBoundary92PercentTaxIdx2Bear() public view {
        // deltaS=8, effIdx=2 → penalty=512*2/20=51 >= 50 → BEAR
        (uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.calculateParams(_inputs(_pct(92), _norm(2)));
        _assertBear(ci, as_, aw, dd);
    }

    function testAt95PercentTaxIdx7() public view {
        // deltaS=5, effIdx=7 → penalty=125*7/20=43 < 50 → BULL
        (uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.calculateParams(_inputs(_pct(95), _norm(7)));
        _assertBull(ci, as_, aw, dd);
    }

    function testAt95PercentTaxIdx8Bear() public view {
        // deltaS=5, effIdx=8 → penalty=125*8/20=50 NOT < 50 → BEAR
        (uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.calculateParams(_inputs(_pct(95), _norm(8)));
        _assertBear(ci, as_, aw, dd);
    }

    function testAt97PercentHighTax() public view {
        // deltaS=3, effIdx=29 → penalty=27*29/20=39 < 50 → BULL
        (uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.calculateParams(_inputs(_pct(97), _norm(29)));
        _assertBull(ci, as_, aw, dd);
    }

    function testAt100PercentAlwaysBull() public view {
        for (uint256 t = 0; t < 30; t++) {
            (uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.calculateParams(_inputs(1e18, _norm(t)));
            _assertBull(ci, as_, aw, dd);
        }
    }

    function testEffIdxShiftAtBoundary() public view {
        // taxIdx=13: effIdx=13, penalty=64*13/20=41 < 50 → BULL
        {
            (uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.calculateParams(_inputs(_pct(96), _norm(13)));
            _assertBull(ci, as_, aw, dd);
        }
        // taxIdx=14: effIdx=15 (shift!), penalty=64*15/20=48 < 50 → BULL
        {
            (uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.calculateParams(_inputs(_pct(96), _norm(14)));
            _assertBull(ci, as_, aw, dd);
        }
        // taxIdx=15: effIdx=16, penalty=64*16/20=51 >= 50 → BEAR
        {
            (uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.calculateParams(_inputs(_pct(96), _norm(15)));
            _assertBear(ci, as_, aw, dd);
        }
    }

    // ---- Unused slots are ignored ----

    function testUnusedSlotsIgnored() public view {
        // Populate slots 2-7 with arbitrary values; output should be unchanged.
        OptimizerInput[8] memory inp;
        inp[0] = OptimizerInput({mantissa: int256(_pct(92)), shift: 0});
        inp[1] = OptimizerInput({mantissa: int256(_norm(0)), shift: 0});
        inp[2] = OptimizerInput({mantissa: 12345678, shift: 0});
        inp[3] = OptimizerInput({mantissa: 9876, shift: 0});
        inp[4] = OptimizerInput({mantissa: 1e17, shift: 0});
        inp[5] = OptimizerInput({mantissa: 3600, shift: 0});
        inp[6] = OptimizerInput({mantissa: 5e17, shift: 0});
        inp[7] = OptimizerInput({mantissa: 42, shift: 0});

        (uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.calculateParams(inp);
        _assertBull(ci, as_, aw, dd);
    }

    // ---- Negative mantissa guards ----

    function testNegativeMantissaSlot0Reverts() public {
        OptimizerInput[8] memory inp;
        inp[0] = OptimizerInput({mantissa: -1, shift: 0});
        vm.expectRevert("negative mantissa");
        push3.calculateParams(inp);
    }

    function testNegativeMantissaSlot1Reverts() public {
        OptimizerInput[8] memory inp;
        inp[0] = OptimizerInput({mantissa: int256(_pct(95)), shift: 0});
        inp[1] = OptimizerInput({mantissa: -1, shift: 0});
        vm.expectRevert("negative mantissa");
        push3.calculateParams(inp);
    }

    function testNegativeMantissaSlot5Reverts() public {
        OptimizerInput[8] memory inp;
        inp[5] = OptimizerInput({mantissa: -1, shift: 0});
        vm.expectRevert("negative mantissa");
        push3.calculateParams(inp);
    }

    // ---- Shift guard ----

    function testNonZeroShiftReverts() public {
        for (uint256 k = 0; k < 8; k++) {
            OptimizerInput[8] memory inp;
            inp[k] = OptimizerInput({mantissa: 0, shift: 1});
            vm.expectRevert("shift not yet supported");
            push3.calculateParams(inp);
        }
    }

    // ---- Fuzz ----

    function testFuzzNeverReverts(uint256 percentageStaked, uint256 averageTaxRate) public view {
        percentageStaked = bound(percentageStaked, 0, 1e18);
        averageTaxRate = bound(averageTaxRate, 0, 1e18);
        push3.calculateParams(_inputs(percentageStaked, averageTaxRate));
    }

    function testFuzzOutputsAreAlwaysBearOrBull(uint256 percentageStaked, uint256 averageTaxRate) public view {
        percentageStaked = bound(percentageStaked, 0, 1e18);
        averageTaxRate = bound(averageTaxRate, 0, 1e18);
        (uint256 ci, uint256 as_, uint24 aw, uint256 dd) =
            push3.calculateParams(_inputs(percentageStaked, averageTaxRate));

        // CI is always 0 in the binary bear/bull model
        assertEq(ci, 0, "ci always 0");

        // Output is exactly BEAR or BULL
        bool isBearOutput = (as_ == BEAR_ANCHOR_SHARE && aw == BEAR_ANCHOR_WIDTH && dd == BEAR_DISCOVERY);
        bool isBullOutput = (as_ == BULL_ANCHOR_SHARE && aw == BULL_ANCHOR_WIDTH && dd == BULL_DISCOVERY);
        assertTrue(isBearOutput || isBullOutput, "output must be bear or bull");
    }
}
