"""Backend tests for CryptoEditorial API."""
import os
import time
import uuid
import pytest
import requests
from pathlib import Path

# Read REACT_APP_BACKEND_URL from frontend/.env (required by env spec)
def _load_backend_url():
    fe_env = Path("/app/frontend/.env")
    for line in fe_env.read_text().splitlines():
        if line.startswith("REACT_APP_BACKEND_URL="):
            return line.split("=", 1)[1].strip().strip('"').rstrip("/")
    raise RuntimeError("REACT_APP_BACKEND_URL not found")

BASE_URL = _load_backend_url()
API = f"{BASE_URL}/api"

ADMIN_EMAIL = "admin@cryptoeditorial.id"
ADMIN_PASSWORD = "Admin123!"

# Unique trader for this run to avoid clashes across test iterations
RUN_ID = uuid.uuid4().hex[:8]
TRADER_EMAIL = f"trader_{RUN_ID}@test.com"
TRADER_PASSWORD = "Test123!"


# ---------- Health ----------
class TestHealth:
    def test_root(self):
        r = requests.get(f"{API}/", timeout=15)
        assert r.status_code == 200
        data = r.json()
        assert data.get("status") == "ok"


# ---------- Auth ----------
class TestAuth:
    def test_register_trader(self):
        s = requests.Session()
        r = s.post(f"{API}/auth/register",
                   json={"email": TRADER_EMAIL, "password": TRADER_PASSWORD, "name": "Trader Test"},
                   timeout=15)
        assert r.status_code == 200, r.text
        body = r.json()
        assert body["email"] == TRADER_EMAIL
        assert body["role"] == "user"
        assert "id" in body
        # cookies set
        cookies = s.cookies.get_dict()
        assert "access_token" in cookies
        assert "refresh_token" in cookies

    def test_register_duplicate_email(self):
        r = requests.post(f"{API}/auth/register",
                          json={"email": TRADER_EMAIL, "password": TRADER_PASSWORD, "name": "Dup"},
                          timeout=15)
        assert r.status_code == 400

    def test_login_admin_and_me(self):
        s = requests.Session()
        r = s.post(f"{API}/auth/login",
                   json={"email": ADMIN_EMAIL, "password": ADMIN_PASSWORD}, timeout=15)
        assert r.status_code == 200, r.text
        body = r.json()
        assert body["email"] == ADMIN_EMAIL
        assert body["role"] == "admin"
        assert "access_token" in s.cookies.get_dict()
        # /auth/me with cookies
        me = s.get(f"{API}/auth/me", timeout=15)
        assert me.status_code == 200
        assert me.json()["email"] == ADMIN_EMAIL
        assert "password_hash" not in me.json()

    def test_login_trader(self):
        s = requests.Session()
        r = s.post(f"{API}/auth/login",
                   json={"email": TRADER_EMAIL, "password": TRADER_PASSWORD}, timeout=15)
        assert r.status_code == 200
        assert r.json()["email"] == TRADER_EMAIL

    def test_me_unauthenticated(self):
        r = requests.get(f"{API}/auth/me", timeout=15)
        assert r.status_code == 401

    def test_login_invalid_password(self):
        # use unique identifier so we don't trigger lockout for other tests
        email = f"noexist_{uuid.uuid4().hex[:6]}@test.com"
        r = requests.post(f"{API}/auth/login",
                          json={"email": email, "password": "wrong"}, timeout=15)
        assert r.status_code == 401

    def test_refresh_token(self):
        s = requests.Session()
        login = s.post(f"{API}/auth/login",
                       json={"email": ADMIN_EMAIL, "password": ADMIN_PASSWORD}, timeout=15)
        assert login.status_code == 200
        # Drop access cookie to force refresh path uses refresh_token
        s.cookies.pop("access_token", None)
        r = s.post(f"{API}/auth/refresh", timeout=15)
        assert r.status_code == 200
        new_access = s.cookies.get("access_token")
        assert new_access  # cookie re-set
        # Verify the new access token actually authenticates
        me = s.get(f"{API}/auth/me", timeout=15)
        assert me.status_code == 200
        assert me.json()["email"] == ADMIN_EMAIL

    def test_logout(self):
        s = requests.Session()
        s.post(f"{API}/auth/login",
               json={"email": ADMIN_EMAIL, "password": ADMIN_PASSWORD}, timeout=15)
        r = s.post(f"{API}/auth/logout", timeout=15)
        assert r.status_code == 200
        # After logout, /me should fail
        me = s.get(f"{API}/auth/me", timeout=15)
        assert me.status_code == 401

    def test_brute_force_lockout(self):
        # Use unique email so this test doesn't lock real accounts
        email = f"brute_{uuid.uuid4().hex[:6]}@test.com"
        statuses = []
        for _ in range(6):
            r = requests.post(f"{API}/auth/login",
                              json={"email": email, "password": "x"}, timeout=15)
            statuses.append(r.status_code)
        # First 5 should be 401, 6th should be 429 (locked)
        assert statuses[-1] == 429, f"expected lockout 429, got {statuses}"


# ---------- Crypto (CoinGecko proxy) ----------
class TestCrypto:
    def test_markets(self):
        r = requests.get(f"{API}/crypto/markets", params={"per_page": 5}, timeout=30)
        if r.status_code == 503:
            pytest.skip("CoinGecko rate-limited (external)")
        assert r.status_code == 200, r.text
        data = r.json()
        assert isinstance(data, list)
        assert len(data) <= 5 and len(data) > 0
        c = data[0]
        for key in ("id", "symbol", "current_price", "sparkline_in_7d",
                    "price_change_percentage_24h_in_currency",
                    "price_change_percentage_7d_in_currency",
                    "price_change_percentage_30d_in_currency"):
            assert key in c, f"missing {key}"

    @pytest.mark.parametrize("period", ["24h", "7d", "30d"])
    def test_top_gainers(self, period):
        r = requests.get(f"{API}/crypto/top-gainers",
                         params={"period": period, "limit": 10}, timeout=30)
        if r.status_code == 503:
            pytest.skip("CoinGecko rate-limited")
        assert r.status_code == 200, r.text
        data = r.json()
        assert isinstance(data, list)
        assert len(data) <= 10 and len(data) > 0
        # verify sorted desc by selected period
        field = {"24h": "price_change_percentage_24h_in_currency",
                 "7d": "price_change_percentage_7d_in_currency",
                 "30d": "price_change_percentage_30d_in_currency"}[period]
        vals = [c.get(field) for c in data]
        assert vals == sorted(vals, reverse=True)

    def test_coin_detail(self):
        r = requests.get(f"{API}/crypto/coin/bitcoin", timeout=30)
        if r.status_code == 503:
            pytest.skip("CoinGecko rate-limited")
        assert r.status_code == 200
        data = r.json()
        assert data["id"] == "bitcoin"
        assert data["symbol"] == "btc"
        assert "usd" in data["current_price"]
        assert "idr" in data["current_price"]
        assert isinstance(data.get("sparkline_7d"), list)
        assert "ath" in data and "atl" in data

    def test_chart(self):
        r = requests.get(f"{API}/crypto/chart/bitcoin", params={"days": 30}, timeout=30)
        if r.status_code == 503:
            pytest.skip("CoinGecko rate-limited")
        assert r.status_code == 200
        prices = r.json().get("prices")
        assert isinstance(prices, list) and len(prices) > 5
        assert "t" in prices[0] and "p" in prices[0]

    def test_search(self):
        r = requests.get(f"{API}/crypto/search", params={"query": "sol"}, timeout=30)
        if r.status_code == 503:
            pytest.skip("CoinGecko rate-limited")
        assert r.status_code == 200
        coins = r.json().get("coins")
        assert isinstance(coins, list) and len(coins) <= 15 and len(coins) > 0

    def test_exchange_rate(self):
        r = requests.get(f"{API}/crypto/exchange-rate", timeout=30)
        if r.status_code == 503:
            pytest.skip("CoinGecko rate-limited")
        assert r.status_code == 200
        data = r.json()
        assert "usd" in data and "idr" in data


# ---------- AI Analyze ----------
class TestAI:
    def test_ai_analyze_bitcoin(self):
        r = requests.post(f"{API}/ai/analyze",
                          json={"coin_id": "bitcoin", "horizon": "1m"}, timeout=120)
        if r.status_code == 503:
            pytest.skip("CoinGecko rate-limited")
        assert r.status_code == 200, r.text
        data = r.json()
        assert "coin" in data and data["coin"]["id"] == "bitcoin"
        assert "analysis" in data
        a = data["analysis"]
        assert a.get("signal") in ("BUY", "HOLD", "SELL")
        assert "confidence" in a
        assert "summary" in a
        assert "buy_zone" in a and "sell_zone" in a and "stop_loss" in a
        assert "key_factors" in a and "risks" in a and "timing_advice" in a
        assert "disclaimer" in data


# ---------- Portfolio ----------
@pytest.fixture(scope="module")
def trader_session():
    s = requests.Session()
    # Try login first; if user doesn't exist, register
    r = s.post(f"{API}/auth/login",
               json={"email": TRADER_EMAIL, "password": TRADER_PASSWORD}, timeout=15)
    if r.status_code != 200:
        s.post(f"{API}/auth/register",
               json={"email": TRADER_EMAIL, "password": TRADER_PASSWORD, "name": "Trader"},
               timeout=15)
    return s


class TestPortfolio:
    def test_holdings_requires_auth(self):
        r = requests.get(f"{API}/portfolio/holdings", timeout=15)
        assert r.status_code == 401

    def test_create_holding_requires_auth(self):
        r = requests.post(f"{API}/portfolio/holdings",
                          json={"coin_id": "bitcoin", "symbol": "btc", "name": "Bitcoin",
                                "quantity": 0.1, "buy_price_usd": 30000, "note": "x"},
                          timeout=15)
        assert r.status_code == 401

    def test_create_and_list_holding(self, trader_session):
        s = trader_session
        payload = {"coin_id": "bitcoin", "symbol": "btc", "name": "Bitcoin",
                   "quantity": 0.5, "buy_price_usd": 25000, "note": "TEST entry"}
        r = s.post(f"{API}/portfolio/holdings", json=payload, timeout=20)
        assert r.status_code == 200, r.text
        h = r.json()
        assert "id" in h
        assert "_id" not in h
        assert "user_id" not in h
        assert h["coin_id"] == "bitcoin"
        assert h["symbol"] == "BTC"

        # GET enriched list
        r2 = s.get(f"{API}/portfolio/holdings", timeout=30)
        if r2.status_code == 503:
            pytest.skip("CoinGecko rate-limited for enrichment")
        assert r2.status_code == 200
        body = r2.json()
        assert "holdings" in body and "summary" in body
        ids = [x["id"] for x in body["holdings"]]
        assert h["id"] in ids
        # Find our holding and verify enrichment fields
        ours = next(x for x in body["holdings"] if x["id"] == h["id"])
        for k in ("current_price_usd", "pnl_usd", "pnl_pct", "change_24h",
                  "current_value_usd", "invested_usd"):
            assert k in ours
        for k in ("total_invested_usd", "total_value_usd", "pnl_usd", "pnl_pct"):
            assert k in body["summary"]

    def test_delete_holding(self, trader_session):
        s = trader_session
        # Create and delete
        r = s.post(f"{API}/portfolio/holdings",
                   json={"coin_id": "ethereum", "symbol": "eth", "name": "Ethereum",
                         "quantity": 1.0, "buy_price_usd": 1500, "note": "TEST del"},
                   timeout=20)
        assert r.status_code == 200
        hid = r.json()["id"]
        d = s.delete(f"{API}/portfolio/holdings/{hid}", timeout=15)
        assert d.status_code == 200
        # Delete again -> 404
        d2 = s.delete(f"{API}/portfolio/holdings/{hid}", timeout=15)
        assert d2.status_code == 404

    def test_delete_nonexistent_holding(self, trader_session):
        s = trader_session
        r = s.delete(f"{API}/portfolio/holdings/{uuid.uuid4()}", timeout=15)
        assert r.status_code == 404


# ---------- Mongo: indexes & admin seed ----------
class TestMongoSetup:
    @pytest.fixture(scope="class")
    def db(self):
        from motor.motor_asyncio import AsyncIOMotorClient
        import asyncio
        url = None
        for line in Path("/app/backend/.env").read_text().splitlines():
            if line.startswith("MONGO_URL="):
                url = line.split("=", 1)[1].strip().strip('"')
            if line.startswith("DB_NAME="):
                dbn = line.split("=", 1)[1].strip().strip('"')
        cli = AsyncIOMotorClient(url)
        return cli[dbn]

    def test_indexes_present(self, db):
        import asyncio
        async def _run():
            users_idx = await db.users.index_information()
            holdings_idx = await db.holdings.index_information()
            login_idx = await db.login_attempts.index_information()
            return users_idx, holdings_idx, login_idx
        users_idx, holdings_idx, login_idx = asyncio.get_event_loop().run_until_complete(_run())
        # email unique
        email_idx = [v for k, v in users_idx.items() if any(f[0] == "email" for f in v.get("key", []))]
        assert email_idx, f"users.email index missing: {users_idx}"
        assert email_idx[0].get("unique") is True
        # holdings.user_id
        assert any(any(f[0] == "user_id" for f in v.get("key", [])) for v in holdings_idx.values()), holdings_idx
        # login_attempts.identifier
        assert any(any(f[0] == "identifier" for f in v.get("key", [])) for v in login_idx.values()), login_idx

    def test_admin_seeded_with_bcrypt(self, db):
        import asyncio
        async def _run():
            return await db.users.find_one({"email": ADMIN_EMAIL})
        admin = asyncio.get_event_loop().run_until_complete(_run())
        assert admin is not None, "admin not seeded"
        assert admin["role"] == "admin"
        ph = admin["password_hash"]
        assert ph.startswith("$2b$") or ph.startswith("$2a$") or ph.startswith("$2y$"), f"bcrypt prefix wrong: {ph[:4]}"
