import json
import subprocess
from pathlib import Path
from textwrap import dedent
import pytest

ROOT = Path(__file__).parents[1]


def run_php(source):
    return json.loads(subprocess.run(["php", "-r", source], cwd=ROOT, text=True, capture_output=True, check=True).stdout)


def run_fill_fixture(is_linear):
    instrument_flag = 1 if is_linear else 0
    source = dedent(
        f"""
        class ErrorHandlerBee {{
            public static function catchNull($value) {{ return $value; }}
            public static function isTrue($value) {{ if (!$value) throw new RuntimeException('assertion'); return $value; }}
        }}
        class DataOutput {{ public static function publishDataToChannel(...$args) {{}} }}
        require {json.dumps(str(ROOT / 'classes' / 'ResultsV2Observer.php'))};
        require {json.dumps(str(ROOT / 'classes' / 'Order.php'))};
        class FillTestDouble {{
            public array $feeBases = [];
            public array $equityUpdates = [];
            public function updatePosition($position, $average) {{}}
            public function addFee($type, $base) {{
                $this->feeBases[] = [$type, $base];
                return round(abs($base) * 0.0005, 8);
            }}
            public function updateEquity($equity) {{ $this->equityUpdates[] = $equity; }}
            public function CalcFinalAmount() {{}}
        }}
        class FillRedisDouble {{
            public function jsonset(...$args) {{ return true; }}
            public function jsonget(...$args) {{
                return '[{{"id":"o1","amount":2,"side":0,"price":80,"type":2,"label":"close_position_order","status":1}}]';
            }}
            public function disconnect() {{}}
        }}
        class FillLogDouble {{
            public array $entries = [];
            public function add(...$args) {{ $this->entries[] = $args; }}
        }}
        $reflection = new ReflectionClass(Order::class);
        $order = $reflection->newInstanceWithoutConstructor();
        $test = new FillTestDouble();
        $redis = new FillRedisDouble();
        $log = new FillLogDouble();
        foreach ([['test', $test], ['redis', $redis], ['transactionLog', $log], ['test_id', 1]] as [$name, $value]) {{
            $property = new ReflectionProperty(Order::class, $name);
            $property->setAccessible(true);
            $property->setValue($order, $value);
        }}
        $state = [
            'id' => 1,
            'timestamp' => 10,
            'position' => 2,
            'average_price' => 100,
            'mark_price' => 80,
            'equity' => 100,
            'fee' => 0,
        ];
        $order->fill(
            $state,
            ['id' => 'o1', 'amount' => 2, 'side' => 0, 'price' => 80, 'type' => 2, 'label' => 'close_position_order'],
            ['id' => 7, 'type' => 'future', 'is_linear' => {instrument_flag}]
        );
        echo json_encode([$state, $test->feeBases, $log->entries]);
        """
    )
    return run_php(source)


def test_actual_order_fill_uses_linear_realized_pnl_and_quote_fee_units():
    state, fee_bases, transactions = run_fill_fixture(True)
    assert fee_bases == [['market', 160]]
    assert state['position'] == 0
    assert state['fee'] == pytest.approx(0.08)
    assert state['equity'] == pytest.approx(59.92)
    assert transactions[0][8] == pytest.approx(-40.08)


def test_actual_order_fill_preserves_inverse_realized_pnl_and_base_fee_units():
    state, fee_bases, transactions = run_fill_fixture(False)
    assert fee_bases == [['market', 0.025]]
    assert state['position'] == 0
    assert state['fee'] == pytest.approx(0.0000125)
    assert state['equity'] == pytest.approx(99.9949875)
    assert transactions[0][8] == pytest.approx(-0.0050125)


def test_linear_equity_point_and_guard_share_fill_units_and_trusted_instrument_metadata():
    src = dedent(
        f"""
        require {json.dumps(str(ROOT / 'classes' / 'CapitalLossGuard.php'))};
        require {json.dumps(str(ROOT / 'classes' / 'ResultsV2Observer.php'))};
        $state = [
            'id' => 1, 'timestamp' => 10, 'equity' => 99.9, 'fee' => 0.1,
            'position' => 2, 'average_price' => 100, 'mark_price' => 80,
        ];
        $point = ResultsV2Observer::equityPoint($state, ['is_linear' => 1, 'type' => 'future']);
        $guard = new CapitalLossGuard(['enabled' => true, 'mode' => 'amount', 'threshold' => 40]);
        $guard->start(100);
        $guardResult = $guard->observeMarkToMarket(
            $point['wallet_balance'] + $state['fee'],
            $point['unrealized_pnl'],
            $state['fee']
        );
        echo json_encode([$point, $guardResult]);
        """
    )
    point, guard = run_php(src)
    assert point['unrealized_pnl'] == pytest.approx(-40)
    assert point['equity'] == pytest.approx(59.9)
    assert guard['reason'] == 'capital_loss_limit'


def test_inverse_equity_point_and_guard_preserve_base_settlement_units():
    src = dedent(
        f"""
        require {json.dumps(str(ROOT / 'classes' / 'CapitalLossGuard.php'))};
        require {json.dumps(str(ROOT / 'classes' / 'ResultsV2Observer.php'))};
        $state = [
            'id' => 1, 'timestamp' => 10, 'equity' => 99.9949875, 'fee' => 0.0000125,
            'position' => 2, 'average_price' => 100, 'mark_price' => 80,
        ];
        $point = ResultsV2Observer::equityPoint($state, ['is_linear' => 0, 'type' => 'future']);
        $guard = new CapitalLossGuard(['enabled' => true, 'mode' => 'amount', 'threshold' => 0.01001]);
        $guard->start(100);
        $guardResult = $guard->observeMarkToMarket(
            $point['wallet_balance'] + $state['fee'],
            $point['unrealized_pnl'],
            $state['fee']
        );
        echo json_encode([$point, $guardResult]);
        """
    )
    point, guard = run_php(src)
    assert point['unrealized_pnl'] == pytest.approx(-0.005)
    assert point['equity'] == pytest.approx(99.9899875)
    assert guard['reason'] == 'capital_loss_limit'


def test_runtime_guard_uses_post_fee_wallet_once_like_results_equity_point():
    src = dedent(
        f"""
        require {json.dumps(str(ROOT / 'classes' / 'CapitalLossGuard.php'))};
        require {json.dumps(str(ROOT / 'classes' / 'Test.php'))};
        require {json.dumps(str(ROOT / 'classes' / 'ResultsV2Observer.php'))};
        class GuardRedisDouble {{
            public function jsonset(...$args) {{ return true; }}
        }}
        $reflection = new ReflectionClass(Test::class);
        $test = $reflection->newInstanceWithoutConstructor();
        foreach ([['redis', new GuardRedisDouble()], ['test_id', 1]] as [$name, $value]) {{
            $property = new ReflectionProperty(Test::class, $name);
            $property->setAccessible(true);
            $property->setValue($test, $value);
        }}
        $state = [
            'id' => 1, 'start_equity' => 100, 'equity' => 99.9, 'fee' => 0.1,
            'unrealized_pnl' => -40, 'position' => 2, 'average_price' => 100,
            'mark_price' => 80, 'capital_loss_guard' => [
                'enabled' => true, 'mode' => 'amount', 'threshold' => 40,
            ],
        ];
        $point = ResultsV2Observer::equityPoint($state, ['is_linear' => 1]);
        $result = $test->applyCapitalLossGuard($state);
        echo json_encode([$point['equity'], $state['capital_loss_guard_snapshot']['capital_loss_guard_last_equity'], $result['reason']]);
        """
    )
    point_equity, guard_equity, reason = run_php(src)
    assert point_equity == pytest.approx(59.9)
    assert guard_equity == pytest.approx(59.9)
    assert reason == 'capital_loss_limit'


def test_percent_and_amount_floors_and_equality_trigger():
    src = "require 'classes/CapitalLossGuard.php'; $p=new CapitalLossGuard(['enabled'=>true,'mode'=>'percent','threshold'=>10]); $p->start(100); $a=new CapitalLossGuard(['enabled'=>true,'mode'=>'amount','threshold'=>25]); $a->start(100); echo json_encode([$p->floor(),$p->observe(90)['reason'],$a->floor(),$a->observe(75)['reason']]);"
    assert run_php(src) == [90, 'capital_loss_limit', 75, 'capital_loss_limit']


def test_baseline_snapshot_and_fail_closed_unavailable_equity():
    src = "require 'classes/CapitalLossGuard.php'; $g=new CapitalLossGuard(['enabled'=>true,'mode'=>'percent','threshold'=>5]); $g->start(200); $s=$g->snapshot(); $r=CapitalLossGuard::fromSnapshot($s); echo json_encode([$r->floor(),$r->observe(null)['reason'],$r->blocksNewExposure()]);"
    assert run_php(src) == [190, 'risk_data_unavailable', True]


def test_unavailable_guard_cancels_and_flattens_before_remaining_fail_closed():
    src = """
    require 'classes/CapitalLossGuard.php';
    $calls = [];
    $position = 1;
    $guard = new CapitalLossGuard(['enabled' => true, 'mode' => 'percent', 'threshold' => 5]);
    $guard->start(100);
    $result = $guard->observe(
        null,
        function () use (&$calls) { $calls[] = 'cancel'; return true; },
        function () use (&$calls, &$position) { $calls[] = 'close'; $position = 0; return true; },
        function () use (&$calls, &$position) { $calls[] = 'flat'; return $position === 0; }
    );
    echo json_encode([$result['reason'], $result['status'], $calls]);
    """
    assert run_php(src) == [
        'risk_data_unavailable',
        'risk_data_unavailable',
        ['cancel', 'close', 'flat'],
    ]


def test_partial_protective_callbacks_never_confirm_a_guard_terminal():
    src = """
    require 'classes/CapitalLossGuard.php';
    $guard = new CapitalLossGuard(['enabled' => true, 'mode' => 'amount', 'threshold' => 1]);
    $guard->start(10);
    $result = $guard->observe(
        9,
        function () { return true; },
        function () { return true; },
        null
    );
    echo json_encode([$result['reason'], $result['status']]);
    """
    assert run_php(src) == ['risk_stop_pending', 'risk_stop_pending']


def test_mark_to_market_equity_includes_unrealized_pnl_and_fee():
    src = "require 'classes/CapitalLossGuard.php'; $g=new CapitalLossGuard(['enabled'=>true,'mode'=>'amount','threshold'=>10]); $g->start(100); echo json_encode($g->observeMarkToMarket(100,-8,2)['reason']);"
    assert run_php(src) == 'capital_loss_limit'


def test_callback_failure_reports_risk_stop_pending_not_false_success():
    src = "require 'classes/CapitalLossGuard.php'; $g=new CapitalLossGuard(['enabled'=>true,'mode'=>'amount','threshold'=>1]); $g->start(10); $r=$g->observe(9, function(){throw new RuntimeException('cancel failed');}); echo json_encode($r['reason']);"
    assert run_php(src) == 'risk_stop_pending'


def test_pending_guard_retries_after_equity_recovers_before_flat_confirmation():
    src = """
    require 'classes/CapitalLossGuard.php';
    $calls = [];
    $guard = new CapitalLossGuard(['enabled' => true, 'mode' => 'amount', 'threshold' => 1]);
    $guard->start(10);
    $initial = $guard->observe(
        9,
        function () use (&$calls) { $calls[] = 'cancel'; return true; },
        function () use (&$calls) { $calls[] = 'close'; return false; },
        function () { return false; }
    );
    $recovered = $guard->observe(
        10,
        function () use (&$calls) { $calls[] = 'cancel2'; return true; },
        function () use (&$calls) { $calls[] = 'close2'; return true; },
        function () { return true; }
    );
    echo json_encode([$initial['reason'], $recovered['reason'], $calls]);
    """
    assert run_php(src) == [
        'risk_stop_pending',
        'capital_loss_limit',
        ['cancel', 'close', 'cancel2', 'close2'],
    ]


def test_pending_guard_retries_cancellation_before_any_recovered_close():
    src = """
    require 'classes/CapitalLossGuard.php';
    $calls = [];
    $guard = new CapitalLossGuard(['enabled' => true, 'mode' => 'amount', 'threshold' => 1]);
    $guard->start(10);
    $initial = $guard->observe(
        9,
        function () use (&$calls) { $calls[] = 'cancel'; return false; },
        function () use (&$calls) { $calls[] = 'close'; return true; },
        function () { return true; }
    );
    $recovered = $guard->observe(
        10,
        function () use (&$calls) { $calls[] = 'cancel2'; return false; },
        function () use (&$calls) { $calls[] = 'close2'; return true; },
        function () { return true; }
    );
    $confirmed = $guard->observe(
        10,
        function () use (&$calls) { $calls[] = 'cancel3'; return true; },
        function () use (&$calls) { $calls[] = 'close3'; return true; },
        function () { return true; }
    );
    echo json_encode([$initial['reason'], $recovered['reason'], $confirmed['reason'], $calls]);
    """
    assert run_php(src) == [
        'risk_stop_pending',
        'risk_stop_pending',
        'capital_loss_limit',
        ['cancel', 'cancel2', 'cancel3', 'close3'],
    ]


def test_order_feeds_inverse_unrealized_pnl_and_cumulative_fee_to_guard():
    order = (ROOT / 'classes/Order.php').read_text(encoding='utf-8')
    assert "test_data['unrealized_pnl']" in order
    assert "test_data['fee']" in order
    assert 'ResultsV2Observer::unrealizedPnl' in order


def test_order_runtime_unavailable_path_passes_protective_callbacks_to_guard():
    order = (ROOT / 'classes/Order.php').read_text(encoding='utf-8')
    helper_start = order.index('private function applyCapitalLossGuardWithProtection')
    helper = order[helper_start:order.index('public function fillThis', helper_start)]

    assert 'return $this->cancelAll();' in helper
    assert '$this->ClosePosition($test_data, $instrument_data);' in helper
    assert "['risk_data_unavailable', 'risk_stop_pending']" in helper
    assert order.count('applyCapitalLossGuardWithProtection(') >= 3


def test_adverse_no_fill_mark_recomputes_inverse_mtm_and_triggers_equality_guard():
    order = (ROOT / 'classes/Order.php').read_text(encoding='utf-8')
    assert order.count('ResultsV2Observer::unrealizedPnl') >= 3
    src = "require 'classes/CapitalLossGuard.php'; require 'classes/ResultsV2Observer.php'; $data=['position'=>10,'average_price'=>100,'mark_price'=>80,'equity'=>100]; $u=ResultsV2Observer::unrealizedPnl($data, ['is_linear'=>0]); $g=new CapitalLossGuard(['enabled'=>true,'mode'=>'amount','threshold'=>'0.025']); $g->start(100); echo json_encode([$u,$g->observeMarkToMarket(100,$u,0)['reason']]);"
    result = run_php(src)
    assert result[0] == pytest.approx(-0.025)
    assert result[1] == 'capital_loss_limit'


def test_order_path_invokes_guard_and_marks_terminal_reason_without_normal_end():
    order = (ROOT / 'classes/Order.php').read_text(encoding='utf-8')
    test = (ROOT / 'classes/Test.php').read_text(encoding='utf-8')
    assert order.count('applyCapitalLossGuard') >= 2
    assert 'ClosePosition($test_data, $instrument_data)' in order
    assert "'termination_reason' = 'capital_loss_limit'" not in test  # writes through Redis path
    assert "'capital_loss_guard_status'" in test
    assert 'foreach ($orders as $order)' in order
    assert 'if(!$this->cancel($order))' in order
    assert 'if($this->getOpenOrders())' in order
    assert 'true\n        );' in order  # ClosePosition uses reduce-only add flag
    assert 'risk_stop_pending' in order
    assert 'capital_loss_guard_retry_count' in order


def test_failed_guard_close_has_a_bounded_retry_cap_and_single_alert_evidence():
    order = (ROOT / 'classes/Order.php').read_text(encoding='utf-8')
    assert 'CAPITAL_LOSS_GUARD_MAX_RETRIES = 3' in order
    assert 'if ($retry >= self::CAPITAL_LOSS_GUARD_MAX_RETRIES)' in order
    assert 'capital_loss_guard_alerted' in order


def test_native_bee_close_path_carries_reduce_only_through_private_order_api():
    order = (ROOT / 'classes/Order.php').read_text(encoding='utf-8')
    buy = (ROOT / 'api/private/buy.php').read_text(encoding='utf-8')
    sell = (ROOT / 'api/private/sell.php').read_text(encoding='utf-8')
    assert 'reduce_only' in order
    assert 'opposite_side' in order
    assert 'amount > abs($position)' in order
    assert 'reduce_only' in buy
    assert 'reduce_only' in sell


def test_reduce_only_rejects_same_side_and_oversized_closes_before_order_write():
    src = (
        "require 'classes/Order.php'; "
        "class GuardReduceOnlyTest { public $position; public function __construct($position) { $this->position = $position; } public function get($a=false, $b=false) { return ['position' => $this->position]; } } "
        "$class = new ReflectionClass('Order'); $order = $class->newInstanceWithoutConstructor(); "
        "$test = $class->getProperty('test'); $test->setValue($order, new GuardReduceOnlyTest(3)); "
        "$add = $class->getMethod('add'); "
        "$attempt = function($side, $amount) use ($add, $order) { try { $add->invoke($order, 'close', $side, 1, $amount, 1, 1, 'market', false, true); return 'accepted'; } catch (Throwable $error) { return $error->getMessage(); } }; "
        "echo json_encode([$attempt(1, 3), $attempt(0, 4)]);"
    )
    assert run_php(src) == [
        'invalid reduce-only capital loss guard close',
        'invalid reduce-only capital loss guard close',
    ]


def test_blocked_guard_rejects_every_edit_path_before_order_mutation():
    src = (
        "class ErrorHandlerBee { public static function catchNull($value) { return $value; } } "
        "class DataOutput { public static function publishDataToChannel() {} } "
        "require 'classes/Order.php'; "
        "class EditGuardTest { public function get($a=false, $b=false) { return ['capital_loss_guard_status' => 'risk_stop_pending']; } } "
        "class EditGuardRedis { public $writes=0; public function jsonset($key, $path, $value) { $this->writes++; return true; } public function jsonget($key) { return json_encode(['id' => 'open', 'type' => 2]); } public function disconnect() {} } "
        "$class = new ReflectionClass('Order'); $order = $class->newInstanceWithoutConstructor(); $redis = new EditGuardRedis(); "
        "foreach ([['test', new EditGuardTest()], ['redis', $redis], ['test_id', 42]] as [$name, $value]) { $property = $class->getProperty($name); $property->setValue($order, $value); } "
        "$attempt = function($method) use ($order) { try { if ($method === 'edit') { $order->edit('open', 2, 100); } elseif ($method === 'amount') { $order->editAmount('open', 2); } else { $order->editPrice('open', 100); } return 'accepted'; } catch (Throwable $error) { return $error->getMessage(); } }; "
        "echo json_encode([$attempt('edit'), $attempt('amount'), $attempt('price'), $redis->writes]);"
    )
    assert run_php(src) == [
        'capital_loss_guard blocks order edit',
        'capital_loss_guard blocks order edit',
        'capital_loss_guard blocks order edit',
        0,
    ]


def test_natural_end_does_not_finalize_when_guard_close_remains_pending():
    update = (ROOT / 'api/private/update.php').read_text(encoding='utf-8')
    terminal_block = update[update.index('if($is_done)'):update.index('$candle->freeSingle')]
    pending_response = update[
        update.index('$respond_guard_pending = function'):update.index('$finalize_guard_terminal = function')
    ]
    assert 'capital_loss_guard_status' in terminal_block
    assert 'risk_stop_pending' in terminal_block
    assert 'position' in terminal_block
    assert 'retryCapitalLossClose' in terminal_block
    if '$order->fillThis' in terminal_block:
        assert terminal_block.index('retryCapitalLossClose') < terminal_block.index('$order->fillThis')
    assert '$respond_guard_pending();' in terminal_block
    assert "'result' => 'stopped'" in pending_response


def test_natural_end_guard_retry_success_preserves_guard_terminal_reason():
    update = (ROOT / 'api/private/update.php').read_text(encoding='utf-8')
    pending_start = update.index('if($guard_pending){')
    fill_start = update.index('$order->fillThis', pending_start)
    assert pending_start < fill_start
    pending_block = update[pending_start:fill_start]
    finalizer = update[
        update.index('$finalize_guard_terminal = function'):update.index('$is_done =')
    ]
    pending_response = update[
        update.index('$respond_guard_pending = function'):update.index('$respond_guard_retry_pending = function')
    ]
    assert "$finalize_guard_terminal($guard_terminal_reason);" in pending_block
    assert '$test->setDatabase($db);' in finalizer
    assert '$test->CalcFinalAmount(' in finalizer
    assert '$results_observer->persistTerminal(' in finalizer
    assert "'capital_loss_limit'" in pending_block
    assert 'false' in finalizer
    assert '$test->stop();' in finalizer
    assert "'result' => 'stopped'" in finalizer
    assert "'termination_reason' => $termination_reason" in finalizer
    assert 'exit();' in finalizer


def test_natural_end_unavailable_guard_stops_without_claiming_end_reached():
    update = (ROOT / 'api/private/update.php').read_text(encoding='utf-8')
    unavailable_start = update.index('if($guard_data_unavailable){')
    assert unavailable_start < update.index('$order->fillThis')
    unavailable_block = update[
        unavailable_start:update.index('if($is_done) {', unavailable_start)
    ]

    finalizer = update[
        update.index('$finalize_guard_terminal = function'):update.index('$is_done =')
    ]
    pending_response = update[
        update.index('$respond_guard_pending = function'):update.index('$respond_guard_retry_pending = function')
    ]
    assert "$finalize_guard_terminal('risk_data_unavailable');" in unavailable_block
    assert '$test->recordTermination(' in finalizer
    assert "'risk_data_unavailable'" in unavailable_block
    assert '$order->retryCapitalLossClose(' in unavailable_block
    assert "'risk_data_unavailable'" in unavailable_block
    assert '$respond_guard_pending();' in unavailable_block
    assert "'termination_reason' => 'risk_stop_pending'" in pending_response
    assert '$results_observer->persistTerminal(' in finalizer
    assert 'false' in finalizer
    assert "'result' => 'stopped'" in finalizer
    assert "'termination_reason' => $termination_reason" in finalizer
    assert 'exit();' in finalizer


def test_post_fill_guard_terminal_reconciliation_cannot_return_continue():
    update = (ROOT / 'api/private/update.php').read_text(encoding='utf-8')
    fill_start = update.index('$order->fillThis')
    reconciliation_start = update.index('$post_fill_guard_reason = null;', fill_start)
    normal_end_start = update.index('if($is_done) {', reconciliation_start)
    reconciliation = update[reconciliation_start:normal_end_start]

    assert '$finalize_guard_terminal = function' in update
    assert fill_start < reconciliation_start < normal_end_start
    assert "'capital_loss_limit'" in reconciliation
    assert "'risk_data_unavailable'" in reconciliation
    assert "'risk_stop_pending'" in reconciliation
    assert '$finalize_guard_terminal($post_fill_guard_reason);' in reconciliation
    assert '$respond_guard_retry_pending();' in reconciliation

    finalizer = update[
        update.index('$finalize_guard_terminal = function'):update.index('$is_done =')
    ]
    assert "'result' => 'continue'" not in finalizer
