import json
import subprocess
from pathlib import Path


ROOT = Path(__file__).parents[1]


def test_private_start_loads_and_persists_guard_before_initial_mark_to_market():
    source = (ROOT / "api/private/start.php").read_text(encoding="utf-8")

    assert "$_POST['capital_loss_guard']" in source
    assert "initializeCapitalLossGuard" in source
    assert source.index("initializeCapitalLossGuard") < source.index(
        "$test->UpdateMarkPrice"
    )


def test_runtime_guard_initialization_is_visible_to_apply_capital_loss_guard():
    php = """
    require %s;
    require %s;
    class FakeRedis {
        public array $values = [];
        public function jsonset($key, $path, $value) {
            if ($path === '.') {
                $this->values[$key] = json_decode($value, true);
                return 'OK';
            }
            $field = ltrim($path, '$.');
            $this->values[$key][$field] = is_string($value)
                ? (json_decode($value, true) ?? $value)
                : $value;
            return 'OK';
        }
        public function jsonget($key) {
            return json_encode($this->values[$key] ?? null);
        }
        public function disconnect() {}
    }
    $reflection = new ReflectionClass(Test::class);
    $test = $reflection->newInstanceWithoutConstructor();
    $redis = new FakeRedis();
    $redis->values['test:42'] = [
        'id' => 42,
        'start_equity' => 100,
        'equity' => 90,
        'unrealized_pnl' => 0,
        'fee' => 0,
        'timestamp' => 123,
        'status' => 1,
    ];
    foreach ([['redis', $redis], ['test_id', 42]] as [$name, $value]) {
        $property = new ReflectionProperty(Test::class, $name);
        $property->setAccessible(true);
        $property->setValue($test, $value);
    }
    $test->initializeCapitalLossGuard([
        'enabled' => true,
        'mode' => 'percent',
        'threshold' => '10',
    ]);
    $runtime = json_decode($redis->jsonget('test:42'), true);
    $result = $test->applyCapitalLossGuard($runtime);
    echo json_encode([
        $runtime['capital_loss_guard'],
        $result['reason'],
        $runtime['capital_loss_guard_status'],
    ]);
    """ % (
        json.dumps(str(ROOT / "classes/CapitalLossGuard.php")),
        json.dumps(str(ROOT / "classes/Test.php")),
    )
    completed = subprocess.run(
        ["php", "-r", php], cwd=ROOT, capture_output=True, text=True
    )
    assert completed.returncode == 0, completed.stderr
    assert json.loads(completed.stdout) == [
        {"enabled": True, "mode": "percent", "threshold": 10},
        "capital_loss_limit",
        "risk_stop_pending",
    ]


def test_runtime_unavailable_guard_forwards_protective_callbacks():
    php = """
    require %s;
    require %s;
    class FakeRedisForUnavailableCallbacks {
        public array $values = [];
        public function jsonset($key, $path, $value) {
            $field = ltrim($path, '$.');
            $this->values[$key][$field] = is_string($value)
                ? (json_decode($value, true) ?? $value)
                : $value;
            return 'OK';
        }
        public function jsonget($key) { return json_encode($this->values[$key] ?? null); }
        public function disconnect() {}
    }
    $redis = new FakeRedisForUnavailableCallbacks();
    $redis->values['test:42'] = [
        'id' => 42, 'start_equity' => 100, 'equity' => null,
        'unrealized_pnl' => 0, 'fee' => 0, 'timestamp' => 123,
    ];
    $reflection = new ReflectionClass(Test::class);
    $test = $reflection->newInstanceWithoutConstructor();
    foreach ([['redis', $redis], ['test_id', 42]] as [$name, $value]) {
        $property = new ReflectionProperty(Test::class, $name);
        $property->setAccessible(true);
        $property->setValue($test, $value);
    }
    $test->initializeCapitalLossGuard([
        'enabled' => true, 'mode' => 'percent', 'threshold' => '10',
    ]);
    $runtime = json_decode($redis->jsonget('test:42'), true);
    $calls = [];
    $result = $test->applyCapitalLossGuard(
        $runtime,
        function () use (&$calls) { $calls[] = 'cancel'; return true; },
        function () use (&$calls) { $calls[] = 'close'; return true; },
        function () use (&$calls) { $calls[] = 'flat'; return true; }
    );
    echo json_encode([$result['reason'], $runtime['capital_loss_guard_status'], $calls]);
    """ % (
        json.dumps(str(ROOT / "classes/CapitalLossGuard.php")),
        json.dumps(str(ROOT / "classes/Test.php")),
    )
    completed = subprocess.run(
        ["php", "-r", php], cwd=ROOT, capture_output=True, text=True
    )
    assert completed.returncode == 0, completed.stderr
    assert json.loads(completed.stdout) == [
        "risk_data_unavailable",
        "risk_data_unavailable",
        ["cancel", "close", "flat"],
    ]


def test_valid_equity_recovery_clears_unavailable_trigger_before_later_floor_breach():
    php = """
    require %s;
    require %s;
    class FakeRedisForUnavailableRecovery {
        public array $values = [];
        public function jsonset($key, $path, $value) {
            $field = ltrim($path, '$.');
            $this->values[$key][$field] = is_string($value)
                ? (json_decode($value, true) ?? $value)
                : $value;
            return 'OK';
        }
        public function jsonget($key) { return json_encode($this->values[$key] ?? null); }
        public function jsondel($key, $path) {
            $field = ltrim($path, '$.');
            unset($this->values[$key][$field]);
            return 1;
        }
        public function disconnect() {}
    }
    $redis = new FakeRedisForUnavailableRecovery();
    $redis->values['test:42'] = [
        'id' => 42, 'start_equity' => 100, 'equity' => null,
        'unrealized_pnl' => 0, 'fee' => 0, 'timestamp' => 111, 'status' => 1,
    ];
    $reflection = new ReflectionClass(Test::class);
    $test = $reflection->newInstanceWithoutConstructor();
    foreach ([['redis', $redis], ['test_id', 42]] as [$name, $value]) {
        $property = new ReflectionProperty(Test::class, $name);
        $property->setAccessible(true);
        $property->setValue($test, $value);
    }
    $test->initializeCapitalLossGuard([
        'enabled' => true, 'mode' => 'percent', 'threshold' => '10',
    ]);
    $runtime = json_decode($redis->jsonget('test:42'), true);
    $unavailable = $test->applyCapitalLossGuard($runtime);

    $runtime['equity'] = 100;
    $runtime['timestamp'] = 222;
    $recovered = $test->applyCapitalLossGuard($runtime);

    $runtime['equity'] = 89;
    $runtime['timestamp'] = 333;
    $breach = $test->applyCapitalLossGuard(
        $runtime,
        function () { return true; },
        function () { return true; },
        function () { return true; }
    );
    $persisted = json_decode($redis->jsonget('test:42'), true);
    echo json_encode([
        $unavailable['reason'],
        $recovered['reason'],
        $runtime['capital_loss_guard_trigger_reason'] ?? null,
        $breach['reason'],
        $runtime['capital_loss_guard_status'],
        $runtime['termination_reason'] ?? null,
        $runtime['termination_timestamp'] ?? null,
        $runtime['status'] ?? null,
        $persisted['capital_loss_guard_trigger_reason'] ?? null,
    ]);
    """ % (
        json.dumps(str(ROOT / "classes/CapitalLossGuard.php")),
        json.dumps(str(ROOT / "classes/Test.php")),
    )
    completed = subprocess.run(
        ["php", "-r", php], cwd=ROOT, capture_output=True, text=True
    )
    assert completed.returncode == 0, completed.stderr
    assert json.loads(completed.stdout) == [
        "risk_data_unavailable",
        None,
        None,
        "capital_loss_limit",
        "capital_loss_limit",
        "capital_loss_limit",
        333,
        0,
        None,
    ]


def test_successful_guard_close_persists_terminal_snapshot_for_restart_without_reclose():
    php = """
    require %s;
    require %s;
    require %s;
    class FakeRedisForGuardClose {
        public array $values = [];
        public function jsonset($key, $path, $value) {
            $field = ltrim($path, '$.');
            $this->values[$key][$field] = is_string($value)
                ? (json_decode($value, true) ?? $value)
                : $value;
            return 'OK';
        }
        public function jsonget($key) { return json_encode($this->values[$key] ?? null); }
        public function jsonobjlen($key) { return count($this->values[$key] ?? []); }
        public function jsondel($key, $path) { unset($this->values[$key]); return 1; }
        public function disconnect() {}
    }
    $redis = new FakeRedisForGuardClose();
    $redis->values['test:42'] = [
        'id' => 42, 'start_equity' => 100, 'equity' => 90,
        'unrealized_pnl' => 0, 'fee' => 0, 'timestamp' => 123,
        'status' => 1, 'position' => 0, 'mark_price' => 100,
    ];
    $testReflection = new ReflectionClass(Test::class);
    $test = $testReflection->newInstanceWithoutConstructor();
    foreach ([['redis', $redis], ['test_id', 42]] as [$name, $value]) {
        $property = new ReflectionProperty(Test::class, $name);
        $property->setAccessible(true);
        $property->setValue($test, $value);
    }
    $test->initializeCapitalLossGuard([
        'enabled' => true, 'mode' => 'percent', 'threshold' => '10',
    ]);
    $runtime = json_decode($redis->jsonget('test:42'), true);
    $trigger = $test->applyCapitalLossGuard($runtime);

    $orderReflection = new ReflectionClass(Order::class);
    $order = $orderReflection->newInstanceWithoutConstructor();
    foreach ([['redis', $redis], ['test_id', 42], ['test', $test]] as [$name, $value]) {
        $property = new ReflectionProperty(Order::class, $name);
        $property->setAccessible(true);
        $property->setValue($order, $value);
    }
    $enforce = $orderReflection->getMethod('enforceCapitalLossClose');
    $instrument = ['id' => 1];
    $arguments = [&$runtime, $instrument];
    $enforce->invokeArgs($order, $arguments);

    $restored = CapitalLossGuard::fromSnapshot($runtime['capital_loss_guard_snapshot']);
    $close_calls = 0;
    $after_restart = $restored->observeMarkToMarket(
        90,
        0,
        0,
        null,
        function () use (&$close_calls) { $close_calls++; return true; },
        function () { return true; },
    );
    echo json_encode([
        $trigger['reason'],
        $trigger['status'],
        $runtime['capital_loss_guard_snapshot']['capital_loss_guard_status'],
        $runtime['capital_loss_guard_status'],
        $runtime['termination_reason'],
        $after_restart['reason'],
        $close_calls,
    ]);
    """ % (
        json.dumps(str(ROOT / "classes/CapitalLossGuard.php")),
        json.dumps(str(ROOT / "classes/Test.php")),
        json.dumps(str(ROOT / "classes/Order.php")),
    )
    completed = subprocess.run(
        ["php", "-r", php], cwd=ROOT, capture_output=True, text=True
    )
    assert completed.returncode == 0, completed.stderr
    assert json.loads(completed.stdout) == [
        "capital_loss_limit",
        "risk_stop_pending",
        "capital_loss_limit",
        "capital_loss_limit",
        "capital_loss_limit",
        "capital_loss_limit",
        0,
    ]


def test_unavailable_guard_close_stays_pending_when_an_open_order_cannot_be_cancelled():
    php = """
    require %s;
    require %s;
    require %s;
    class FakeRedisForCancelFailure {
        public array $values = [];
        public function jsonset($key, $path, $value) {
            $field = ltrim($path, '$.');
            $this->values[$key][$field] = is_string($value)
                ? (json_decode($value, true) ?? $value)
                : $value;
            return 'OK';
        }
        public function jsonget($key) { return json_encode($this->values[$key] ?? null); }
        public function jsonobjlen($key) { return count($this->values[$key] ?? []); }
        public function jsondel($key, $path) { return false; }
        public function disconnect() {}
    }
    $redis = new FakeRedisForCancelFailure();
    $redis->values['orders:42'] = [
        'open' => ['id' => 'open', 'status' => 0, 'type' => 2],
    ];
    $testReflection = new ReflectionClass(Test::class);
    $test = $testReflection->newInstanceWithoutConstructor();
    foreach ([['redis', $redis], ['test_id', 42]] as [$name, $value]) {
        $property = new ReflectionProperty(Test::class, $name);
        $property->setAccessible(true);
        $property->setValue($test, $value);
    }
    $orderReflection = new ReflectionClass(Order::class);
    $order = $orderReflection->newInstanceWithoutConstructor();
    foreach ([['redis', $redis], ['test_id', 42], ['test', $test]] as [$name, $value]) {
        $property = new ReflectionProperty(Order::class, $name);
        $property->setAccessible(true);
        $property->setValue($order, $value);
    }
    $runtime = [
        'id' => 42, 'position' => 0, 'timestamp' => 123, 'status' => 1,
        'capital_loss_guard_status' => 'risk_data_unavailable',
        'capital_loss_guard_snapshot' => [
            'capital_loss_guard' => ['enabled' => true, 'mode' => 'percent', 'threshold' => 10],
            'capital_loss_guard_baseline_equity' => 100,
            'capital_loss_guard_status' => 'risk_data_unavailable',
            'capital_loss_guard_last_equity' => 90,
        ],
    ];
    $enforce = $orderReflection->getMethod('enforceCapitalLossClose');
    $instrument = ['id' => 1];
    $arguments = [&$runtime, $instrument];
    $enforce->invokeArgs($order, $arguments);
    echo json_encode([
        $runtime['capital_loss_guard_status'],
        $runtime['capital_loss_guard_retry_count'],
        $runtime['termination_reason'],
        $runtime['capital_loss_guard_snapshot']['capital_loss_guard_status'],
    ]);
    """ % (
        json.dumps(str(ROOT / "classes/Test.php")),
        json.dumps(str(ROOT / "classes/Order.php")),
        json.dumps(str(ROOT / "classes/BeeDiagnosticLog.php")),
    )
    completed = subprocess.run(
        ["php", "-r", php], cwd=ROOT, capture_output=True, text=True
    )
    assert completed.returncode == 0, completed.stderr
    assert json.loads(completed.stdout) == [
        "risk_stop_pending",
        1,
        "risk_stop_pending",
        "risk_stop_pending",
    ]
