"""Backend regression tests for PLTMG Sumbawa II Portal (SQLAlchemy-backed)."""
import os
import re
from pathlib import Path

import pytest
import requests
from dotenv import dotenv_values

frontend_env = dotenv_values("/app/frontend/.env")
base_url = os.environ.get("REACT_APP_BACKEND_URL") or frontend_env.get("REACT_APP_BACKEND_URL")
if not base_url:
    raise RuntimeError("REACT_APP_BACKEND_URL is missing")
BASE_URL = base_url.rstrip("/")
API = f"{BASE_URL}/api"


@pytest.fixture(scope="session")
def test_credentials():
    path = Path("/app/memory/test_credentials.md")
    if not path.exists():
        pytest.skip("Missing /app/memory/test_credentials.md")
    content = path.read_text(encoding="utf-8")
    email = re.search(r'(?im)^\s*(?:[-*]\s*)?(?:\*\*)?email(?:\*\*)?\s*:\s*`?([^`\s]+)', content)
    password = re.search(r'(?im)^\s*(?:[-*]\s*)?(?:\*\*)?password(?:\*\*)?\s*:\s*`?([^`\s]+)', content)
    if not email or not password:
        pytest.skip("No credentials found in test_credentials.md")
    return {"email": email.group(1), "password": password.group(1)}


@pytest.fixture
def client():
    s = requests.Session()
    s.headers.update({"Content-Type": "application/json"})
    return s


@pytest.fixture
def admin_client(client, test_credentials):
    r = client.post(f"{API}/auth/login", json=test_credentials)
    if r.status_code != 200:
        pytest.fail(f"Admin login failed: {r.status_code} {r.text[:300]}")
    if "pltmg_admin" not in client.cookies.get_dict():
        pytest.fail(f"Login did not set pltmg_admin cookie. Cookies: {client.cookies.get_dict()}")
    return client


# ---------------- Public reads ----------------
class TestPublicEndpoints:
    def test_root(self, client):
        r = client.get(f"{BASE_URL}/")
        assert r.status_code == 200

    def test_gallery_list(self, client):
        r = client.get(f"{API}/gallery")
        assert r.status_code == 200
        data = r.json()
        assert isinstance(data, list) and len(data) >= 3
        item = data[0]
        for key in ("id", "title", "caption", "image_url", "category", "created_at"):
            assert key in item
        assert "_id" not in item

    def test_performance_list(self, client):
        r = client.get(f"{API}/performance")
        assert r.status_code == 200
        data = r.json()
        assert isinstance(data, list) and len(data) >= 2
        rec = data[0]
        for key in ("id", "date", "sfc", "sloc", "net_kwh", "fuel_consumption_liters",
                    "avg_daily_load_kw", "running_hours_engine1", "running_hours_engine2",
                    "running_hours_engine3"):
            assert key in rec, f"missing {key}"


# ---------------- Auth ----------------
class TestAuth:
    def test_login_and_me_and_logout(self, client, test_credentials):
        r = client.post(f"{API}/auth/login", json=test_credentials)
        assert r.status_code == 200, r.text
        body = r.json()
        assert body["status"] == "success"
        assert body["email"] == test_credentials["email"]
        assert body["role"] == "admin"
        set_cookie = r.headers.get("set-cookie", "")
        assert "pltmg_admin" in set_cookie
        assert "HttpOnly" in set_cookie
        assert "Secure" in set_cookie
        assert "samesite=none" in set_cookie.lower()

        me = client.get(f"{API}/auth/me")
        assert me.status_code == 200
        assert me.json()["email"] == test_credentials["email"]
        assert me.json()["role"] == "admin"

        out = client.post(f"{API}/auth/logout")
        assert out.status_code == 200
        me2 = client.get(f"{API}/auth/me")
        assert me2.status_code == 401

    def test_me_without_cookie(self, client):
        r = client.get(f"{API}/auth/me")
        assert r.status_code == 401

    def test_login_wrong_password(self, client, test_credentials):
        r = client.post(f"{API}/auth/login", json={"email": test_credentials["email"], "password": "salahbanget"})
        assert r.status_code == 401
        assert "detail" in r.json()

    def test_bad_token_rejected(self, client):
        r = client.get(f"{API}/auth/me", headers={"Authorization": "Bearer garbage.token.here"})
        assert r.status_code == 401

    def test_brute_force_lockout(self, client):
        """Uses a non-admin email so the real admin account stays usable.

        KNOWN ISSUE: the lockout key is `request.client.host` and uvicorn runs without
        --proxy-headers, so through the public ingress the peer IP varies between proxy
        replicas. The 429 therefore does NOT reliably appear on the 6th attempt from
        outside; it takes more attempts and can still be bypassed by rotating requests.
        This test only asserts that a lockout eventually triggers.
        """
        email = "bruteforce_probe@example.test"
        statuses = []
        for _ in range(15):
            r = client.post(f"{API}/auth/login", json={"email": email, "password": "wrong"})
            statuses.append(r.status_code)
        assert set(statuses) <= {401, 429}, statuses
        assert 429 in statuses, f"no lockout observed in 15 attempts: {statuses}"


# ---------------- Gallery CRUD ----------------
class TestGalleryCRUD:
    def test_write_endpoints_require_auth(self, client):
        payload = {"title": "TEST_noauth", "caption": "TEST caption", "image_url": "https://x.test/a.png"}
        assert client.post(f"{API}/gallery", json=payload).status_code == 401
        assert client.put(f"{API}/gallery/1", json=payload).status_code == 401
        assert client.delete(f"{API}/gallery/1").status_code == 401

    def test_full_crud_cycle(self, admin_client):
        payload = {"title": "TEST_Galeri QA", "caption": "TEST caption QA", "image_url": "https://x.test/a.png", "category": "Control Room"}
        r = admin_client.post(f"{API}/gallery", json=payload)
        assert r.status_code == 200, r.text
        created = r.json()
        item_id = created["id"]
        assert created["title"] == payload["title"]
        assert created["category"] == "Control Room"

        listed = admin_client.get(f"{API}/gallery").json()
        assert any(x["id"] == item_id and x["title"] == payload["title"] for x in listed)

        upd = {**payload, "title": "TEST_Galeri QA Updated"}
        r2 = admin_client.put(f"{API}/gallery/{item_id}", json=upd)
        assert r2.status_code == 200, r2.text
        assert r2.json()["title"] == "TEST_Galeri QA Updated"
        listed2 = admin_client.get(f"{API}/gallery").json()
        assert [x for x in listed2 if x["id"] == item_id][0]["title"] == "TEST_Galeri QA Updated"

        r3 = admin_client.delete(f"{API}/gallery/{item_id}")
        assert r3.status_code == 200
        listed3 = admin_client.get(f"{API}/gallery").json()
        assert not any(x["id"] == item_id for x in listed3)

    def test_update_missing_returns_404(self, admin_client):
        r = admin_client.put(f"{API}/gallery/999999", json={"title": "TEST_x", "caption": "TEST_y", "image_url": "https://x.test/a.png"})
        assert r.status_code == 404

    def test_delete_missing_returns_404(self, admin_client):
        assert admin_client.delete(f"{API}/gallery/999999").status_code == 404

    def test_validation_error(self, admin_client):
        r = admin_client.post(f"{API}/gallery", json={"title": "x", "caption": "y", "image_url": "a"})
        assert r.status_code == 422


# ---------------- Performance CRUD ----------------
def perf_payload(**over):
    base = {"date": "2026-07-01", "sfc": 0.199, "sloc": 0.61, "net_kwh": 720000,
            "fuel_consumption_liters": 142800, "avg_daily_load_kw": 30000,
            "running_hours_engine1": 3500, "running_hours_engine2": 3480,
            "running_hours_engine3": 3490}
    base.update(over)
    return base


class TestPerformanceCRUD:
    def test_write_endpoints_require_auth(self, client):
        assert client.post(f"{API}/performance", json=perf_payload()).status_code == 401
        assert client.put(f"{API}/performance/1", json=perf_payload()).status_code == 401
        assert client.delete(f"{API}/performance/1").status_code == 401

    def test_full_crud_and_persistence(self, admin_client):
        payload = perf_payload(net_kwh=744000, avg_daily_load_kw=31000)
        r = admin_client.post(f"{API}/performance", json=payload)
        assert r.status_code == 200, r.text
        rec = r.json()
        rid = rec["id"]
        assert rec["net_kwh"] == 744000
        assert rec["avg_daily_load_kw"] == 31000
        assert rec["sfc"] == payload["sfc"]

        fetched = [x for x in admin_client.get(f"{API}/performance").json() if x["id"] == rid]
        assert len(fetched) == 1
        assert fetched[0]["running_hours_engine1"] == 3500

        upd = perf_payload(net_kwh=768000, avg_daily_load_kw=32000, running_hours_engine1=3600)
        r2 = admin_client.put(f"{API}/performance/{rid}", json=upd)
        assert r2.status_code == 200, r2.text
        assert r2.json()["net_kwh"] == 768000
        after = [x for x in admin_client.get(f"{API}/performance").json() if x["id"] == rid][0]
        assert after["avg_daily_load_kw"] == 32000
        assert after["running_hours_engine1"] == 3600

        assert admin_client.delete(f"{API}/performance/{rid}").status_code == 200
        assert not any(x["id"] == rid for x in admin_client.get(f"{API}/performance").json())

    def test_missing_record_404(self, admin_client):
        assert admin_client.put(f"{API}/performance/999999", json=perf_payload()).status_code == 404
        assert admin_client.delete(f"{API}/performance/999999").status_code == 404

    def test_negative_values_rejected(self, admin_client):
        r = admin_client.post(f"{API}/performance", json=perf_payload(sfc=-1))
        assert r.status_code == 422

    def test_seed_records_intact(self, client):
        data = client.get(f"{API}/performance").json()
        dates = [x["date"] for x in data]
        assert "2026-06-01" in dates and "2026-06-02" in dates
        last = [x for x in data if x["date"] == "2026-06-02"][0]
        assert last["net_kwh"] == 725000


# ---------------- Persistence across restart-safe storage ----------------
class TestSqlPersistence:
    def test_sqlite_file_exists_and_has_tables(self):
        import sqlite3
        db_path = "/app/backend/pltmg.db"
        assert os.path.exists(db_path), "SQLite DB file not created"
        con = sqlite3.connect(db_path)
        tables = {r[0] for r in con.execute("SELECT name FROM sqlite_master WHERE type='table'")}
        con.close()
        assert "gallery_items" in tables
        assert "plant_performance_logs" in tables

    def test_bcrypt_hash_format(self):
        import bcrypt
        h = bcrypt.hashpw(b"x", bcrypt.gensalt())
        assert h.decode().startswith("$2b$")
