"""Minimal deterministic self-test for the paper trading engine.

This is NOT a production test suite. It is a tiny in-memory harness that
exercises the most important invariants and LONG guardrails without requiring
MongoDB or Zerodha.

Run:
  python -m app.v1.services.paper_trading_selftest
"""

from __future__ import annotations

import os
from dataclasses import dataclass
from datetime import datetime
from typing import Any, Dict, Iterable, List, Optional


# Import the module under test.
from app.v1.services import paper_trading


@dataclass
class _InsertResult:
    inserted_id: str


class _UpdateResult:
    def __init__(self, modified_count: int):
        self.modified_count = modified_count


class FakeCollection:
    def __init__(self):
        self._docs: Dict[str, Dict[str, Any]] = {}
        self._id_seq = 1

    def create_index(self, *args, **kwargs):
        return None

    def _new_id(self) -> str:
        _id = str(self._id_seq)
        self._id_seq += 1
        return _id

    def insert_one(self, doc: Dict[str, Any]) -> _InsertResult:
        d = dict(doc)
        _id = d.get("_id") or self._new_id()
        d["_id"] = _id
        self._docs[str(_id)] = d
        return _InsertResult(inserted_id=str(_id))

    def find_one(self, q: Dict[str, Any], proj: Optional[Dict[str, int]] = None) -> Optional[Dict[str, Any]]:
        for d in self.find(q, proj):
            return d
        return None

    def find(self, q: Dict[str, Any], proj: Optional[Dict[str, int]] = None) -> Iterable[Dict[str, Any]]:
        def _match(doc: Dict[str, Any]) -> bool:
            for k, v in (q or {}).items():
                if isinstance(v, dict) and "$in" in v:
                    if doc.get(k) not in set(v["$in"]):
                        return False
                elif isinstance(v, dict) and "$gte" in v:
                    if doc.get(k) is None or float(doc.get(k)) < float(v["$gte"]):
                        return False
                else:
                    if doc.get(k) != v:
                        return False
            return True

        for d in list(self._docs.values()):
            if not _match(d):
                continue
            if proj:
                out = {}
                for k, flag in proj.items():
                    if flag and k in d:
                        out[k] = d[k]
                # always include _id if present
                if "_id" in d:
                    out["_id"] = d["_id"]
                yield out
            else:
                yield dict(d)

    def update_one(self, q: Dict[str, Any], upd: Dict[str, Any], upsert: bool = False) -> _UpdateResult:
        doc = self.find_one(q)
        if doc is None:
            if not upsert:
                return _UpdateResult(0)
            base = {k: v for k, v in (q or {}).items() if not isinstance(v, dict)}
            set_on_insert = (upd or {}).get("$setOnInsert") or {}
            base.update(set_on_insert)
            ins = self.insert_one(base)
            doc = self._docs[ins.inserted_id]

        before = dict(doc)

        for op, payload in (upd or {}).items():
            if op == "$set":
                for k, v in payload.items():
                    doc[k] = v
            elif op == "$inc":
                for k, v in payload.items():
                    doc[k] = float(doc.get(k) or 0.0) + float(v)
            elif op == "$push":
                for k, v in payload.items():
                    arr = doc.get(k)
                    if not isinstance(arr, list):
                        arr = []
                    arr.append(v)
                    doc[k] = arr
            elif op == "$setOnInsert":
                # already applied above
                pass

        self._docs[str(doc["_id"])] = doc

        # emulate match condition for $gte in q (used for available_balance)
        for k, v in (q or {}).items():
            if isinstance(v, dict) and "$gte" in v:
                if before.get(k) is None or float(before.get(k)) < float(v["$gte"]):
                    # revert
                    self._docs[str(doc["_id"])] = before
                    return _UpdateResult(0)

        return _UpdateResult(1)

    def update_many(self, q: Dict[str, Any], upd: Dict[str, Any]) -> _UpdateResult:
        n = 0
        for d in list(self.find(q)):
            r = self.update_one({"_id": d["_id"]}, upd)
            n += r.modified_count
        return _UpdateResult(n)


class FakeDB(dict):
    def __getitem__(self, key: str) -> FakeCollection:
        if key not in self:
            self[key] = FakeCollection()
        return dict.__getitem__(self, key)


def _dt(s: str) -> datetime:
    # naive UTC
    return datetime.fromisoformat(s)


def main() -> None:
    os.environ["PAPER_MARGIN_REQUIRED"] = "false"  # selftest uses notional fallback
    os.environ["PAPER_LONG_PENDING_MAX_CANDLES"] = "2"
    os.environ["PAPER_LONG_OPEN_MAX_CANDLES"] = "1"
    os.environ["PAPER_EOD_CUTOFF_HOUR"] = "15"
    os.environ["PAPER_EOD_CUTOFF_MINUTE"] = "29"

    db = FakeDB()
    user_id = "u1"
    account_id = "a1"

    # Create account
    acc = paper_trading.get_or_create_paper_account(db, user_id=user_id, account_id=account_id)
    assert acc["balance"] > 0

    # LONG regime blocked
    analysis_block = {
        "decision": "BUY",
        "confidence": "VERY_STRONG",
        "entry_price": 100,
        "stop_loss": 95,
        "target": 110,
        "regime": {"trend": "DOWN"},
        "analysis_id": "sig-block",
        "timestamp": "2025-12-23T09:30:00",
    }
    created = paper_trading.create_paper_trade_from_signal(
        db,
        user_id=user_id,
        account_id=account_id,
        stock_id="s1",
        symbol="ABC",
        analysis=analysis_block,
        source="ET",
        market_data={"candles": {"15minute": [{"t": "2025-12-23T09:15:00", "c": 99}]}} ,
        preferred_timeframe="15minute",
    )
    assert created is None

    # LONG allowed => PENDING
    analysis_long = {
        "decision": "BUY",
        "confidence": "VERY_STRONG",
        "entry_price": 100,
        "stop_loss": 95,
        "target": 110,
        "regime": {"trend": "UP"},
        "analysis_id": "sig-long",
        "timestamp": "2025-12-23T09:30:00",
    }
    trade_id = paper_trading.create_paper_trade_from_signal(
        db,
        user_id=user_id,
        account_id=account_id,
        stock_id="s1",
        symbol="XYZ",
        analysis=analysis_long,
        source="ET",
        market_data={"candles": {"15minute": [{"t": "2025-12-23T09:15:00", "c": 99}]}} ,
        preferred_timeframe="15minute",
    )
    assert trade_id is not None

    tr = db[paper_trading.PAPER_TRADES_COLLECTION].find_one({"paper_trade_id": trade_id})
    assert tr is not None
    assert tr["status"] == "PENDING"

    # Candle doesn't confirm
    paper_trading.update_open_paper_trades_for_symbol(
        db,
        symbol="XYZ",
        candle={"t": "2025-12-23T09:30:00", "o": 99, "h": 99.5, "l": 98.5, "c": 99.2},
        candle_timeframe="15minute",
        analysis=analysis_long,
    )
    tr = db[paper_trading.PAPER_TRADES_COLLECTION].find_one({"paper_trade_id": trade_id})
    assert tr["status"] == "PENDING"

    # Candle confirms by OPEN>entry => becomes OPEN
    paper_trading.update_open_paper_trades_for_symbol(
        db,
        symbol="XYZ",
        candle={"t": "2025-12-23T09:45:00", "o": 101, "h": 102, "l": 100.5, "c": 101.2},
        candle_timeframe="15minute",
        analysis=analysis_long,
    )
    tr = db[paper_trading.PAPER_TRADES_COLLECTION].find_one({"paper_trade_id": trade_id})
    assert tr["status"] in {"OPEN", "CLOSED"}

    # With PAPER_LONG_OPEN_MAX_CANDLES=1, next candle triggers TIME_EXIT if still OPEN
    if tr["status"] == "OPEN":
        paper_trading.update_open_paper_trades_for_symbol(
            db,
            symbol="XYZ",
            candle={"t": "2025-12-23T10:00:00", "o": 101, "h": 101.5, "l": 100.8, "c": 101.0},
            candle_timeframe="15minute",
            analysis=analysis_long,
        )
        tr = db[paper_trading.PAPER_TRADES_COLLECTION].find_one({"paper_trade_id": trade_id})
        assert tr["status"] == "CLOSED"
        assert tr.get("exit_reason") in {"TIME_EXIT", "STOP_LOSS", "TARGET", "OPPOSITE_SIGNAL", "EOD_EXIT"}

    # SHORT opens immediately
    analysis_short = {
        "decision": "SELL",
        "confidence": "VERY_STRONG",
        "entry_price": 200,
        "stop_loss": 210,
        "target": 180,
        "analysis_id": "sig-short",
        "timestamp": "2025-12-23T10:15:00",
    }
    short_id = paper_trading.create_paper_trade_from_signal(
        db,
        user_id=user_id,
        account_id=account_id,
        stock_id="s2",
        symbol="SRT",
        analysis=analysis_short,
        source="ET",
        market_data={"candles": {"15minute": [{"t": "2025-12-23T10:15:00", "c": 200}]}} ,
        preferred_timeframe="15minute",
    )
    assert short_id is not None
    tr2 = db[paper_trading.PAPER_TRADES_COLLECTION].find_one({"paper_trade_id": short_id})
    assert tr2["status"] == "OPEN"

    # Opposite VERY_STRONG signal closes trade (after SL/Target checks)
    opp = dict(analysis_long)
    opp["decision"] = "BUY"  # opposite of SHORT
    paper_trading.update_open_paper_trades_for_symbol(
        db,
        symbol="SRT",
        candle={"t": "2025-12-23T10:30:00", "o": 200, "h": 201, "l": 199, "c": 200.5},
        candle_timeframe="15minute",
        analysis=opp,
    )
    tr2 = db[paper_trading.PAPER_TRADES_COLLECTION].find_one({"paper_trade_id": short_id})
    assert tr2["status"] == "CLOSED"
    assert tr2.get("exit_reason") in {"OPPOSITE_SIGNAL", "STOP_LOSS", "TARGET"}

    print("paper_trading_selftest: OK")


if __name__ == "__main__":
    main()
