"""Reference web app test suite (REQ-SYS-13 acceptance evidence).

Run:  python3 -m unittest discover -s tests -v     (or: pytest)
Covers: login lifecycle, adapter round-trip (stub engine), key-absence from every
client-served byte, the outbound-request guard (SSRF), password hashing at rest,
path containment. No network, no real key needed.
"""
import json
import os
import sys
import tempfile
import unittest
import urllib.request
from http.client import HTTPConnection
from pathlib import Path

HERE = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(HERE))

os.environ.setdefault("ADMIN_USER", "admin")
os.environ.setdefault("ADMIN_PASS", "test-password-1234")

from adapters.base import GuardError, guard_url                      # noqa: E402
from adapters.stub import StubAdapter                                # noqa: E402
from app import Handler, create_app                                  # noqa: E402
from users import UserStore, hash_password, verify_password          # noqa: E402


class QuietHandler(Handler):
    def log_message(self, fmt, *args):
        pass


def request(server, method, path, body=None, headers=None):
    conn = HTTPConnection(server[0], server[1], timeout=10)
    try:
        conn.request(method, path,
                     body=json.dumps(body).encode() if body is not None else None,
                     headers={"Content-Type": "application/json", **(headers or {})})
        resp = conn.getresponse()
        raw = resp.read()
        try:
            parsed = json.loads(raw.decode())
        except ValueError:
            parsed = raw
        return resp.status, dict(resp.getheaders()), parsed
    finally:
        conn.close()


import threading
from http.server import ThreadingHTTPServer


class TestApp(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        # seed THE module-level store the handler checks against — a second instance
        # here once masked the exact bug the live demo caught (two stores, one empty)
        import app as webapp
        cls.pw = webapp.store.ensure_admin() or os.environ["ADMIN_PASS"]
        create_app("stub")
        cls.httpd = ThreadingHTTPServer(("127.0.0.1", 0), QuietHandler)
        cls.addr = cls.httpd.server_address
        threading.Thread(target=cls.httpd.serve_forever, daemon=True).start()

    @classmethod
    def tearDownClass(cls):
        cls.httpd.shutdown()

    def login(self):
        status, headers, body = request(self.addr, "POST", "/api/login",
                                        {"username": "admin", "password": self.pw})
        self.assertEqual(status, 200, body)
        cookie = headers.get("Set-Cookie", "").split(";")[0]
        self.assertTrue(cookie.startswith("session="))
        return cookie

    def test_01_home_served_with_login_view(self):
        conn = HTTPConnection(*self.addr, timeout=10)
        conn.request("GET", "/")
        resp = conn.getresponse()
        html = resp.read().decode()
        conn.close()
        self.assertEqual(resp.status, 200)
        self.assertIn("login-form", html)
        self.assertIn("viewport", html)

    def test_02_static_css_served_and_fluid(self):
        conn = HTTPConnection(*self.addr, timeout=10)
        conn.request("GET", "/static/app.css")
        resp = conn.getresponse()
        css = resp.read().decode()
        conn.close()
        self.assertEqual(resp.status, 200)
        self.assertIn("100dvh", css)               # fills the screen — the DEF-093 fix pattern
        self.assertIn("@media", css)               # responsive breakpoints present

    def test_03_static_traversal_refused(self):
        status, _, body = request(self.addr, "GET", "/static/../users.py")
        self.assertEqual(status, 404, body)

    def test_04_chat_requires_login(self):
        status, _, body = request(self.addr, "POST", "/api/chat", {"message": "hi"})
        self.assertEqual(status, 401, body)

    def test_05_wrong_password_refused(self):
        status, _, _ = request(self.addr, "POST", "/api/login",
                               {"username": "admin", "password": "wrong"})
        self.assertEqual(status, 401)

    def test_06_login_then_round_trip(self):
        cookie = self.login()
        status, _, body = request(self.addr, "POST", "/api/chat", {"message": "hello engine"},
                                  headers={"Cookie": cookie})
        self.assertEqual(status, 200, body)
        self.assertIn("You said: hello engine", body["reply"])

    def test_07_no_secret_in_any_client_served_byte(self):
        """Set a canary key; crawl every client-reachable path; the canary must never appear."""
        os.environ["GLM_API_KEY"] = "sk-canary-DO-NOT-LEAK-0123456789abcdef"
        canary = os.environ["GLM_API_KEY"]
        try:
            paths = ["/", "/static/app.css", "/static/app.js", "/static/../app.py", "/static/../users.py"]
            for path in paths:
                conn = HTTPConnection(*self.addr, timeout=10)
                conn.request("GET", path)
                resp = conn.getresponse()
                raw = resp.read()
                conn.close()
                self.assertNotIn(canary.encode(), raw, f"canary leaked via {path}")
            # and the login+chat JSON never carries it either
            cookie = self.login()
            status, _, body = request(self.addr, "POST", "/api/chat", {"message": "x"},
                                      headers={"Cookie": cookie})
            self.assertNotIn(canary, json.dumps(body))
        finally:
            os.environ.pop("GLM_API_KEY", None)

    def test_08_passwords_hashed_at_rest(self):
        store = UserStore()
        self.assertFalse(store.has_plaintext())
        rec = store.users.get("admin", {})
        self.assertIn("$", str(rec.get("hash", "")))

    def test_09_password_hash_roundtrip(self):
        stored = hash_password("s3cret-pw")
        self.assertTrue(verify_password("s3cret-pw", stored))
        self.assertFalse(verify_password("other", stored))


class TestGuard(unittest.TestCase):
    def test_valid_public_host_passes(self):
        host, ip, port = guard_url("https://open.bigmodel.cn/api/paas/v4/chat/completions")
        self.assertEqual(host, "open.bigmodel.cn")
        self.assertEqual(port, 443)

    def test_ftp_refused(self):
        with self.assertRaises(GuardError):
            guard_url("ftp://example.com/x")

    def test_no_host_refused(self):
        with self.assertRaises(GuardError):
            guard_url("https:///path")

    def test_numeric_loopback_refused(self):
        with self.assertRaises(GuardError):
            guard_url("http://127.0.0.1:8000/x")

    def test_private_range_refused(self):
        with self.assertRaises(GuardError):
            guard_url("http://192.168.1.10/x")

    def test_link_local_refused(self):
        with self.assertRaises(GuardError):
            guard_url("http://169.254.169.254/latest/meta-data")   # cloud metadata


class TestStub(unittest.TestCase):
    def test_round_trip_shape(self):
        reply = StubAdapter().chat([{"role": "user", "content": "ping"}])
        self.assertIn("ping", reply)


if __name__ == "__main__":
    unittest.main()
