from __future__ import annotations

import json
import unittest
from pathlib import Path
from typing import Any, Dict

from arr_web.app import PortalApplication, RuntimeHealth
from arr_web.auth import LoginAttemptLedger, LoginCredentials


USERNAME = "finance-operator"
PASSWORD = "correct-horse-battery-staple"
CREDENTIALS = LoginCredentials(USERNAME, PASSWORD)
STATIC_ROOT = Path(__file__).resolve().parents[1] / "arr_web" / "static"


def decoded(response: Any) -> Dict[str, Any]:
    return json.loads(response.body.decode("utf-8"))


def login(
    app: PortalApplication,
    *,
    username: object = USERNAME,
    password: object = PASSWORD,
    client_id: str = "test-client",
) -> Any:
    return app.handle(
        "POST",
        "/api/login",
        {"Content-Type": "application/json"},
        json.dumps({"username": username, "password": password}).encode("utf-8"),
        client_id=client_id,
    )


def auth_headers(response: Any) -> Dict[str, str]:
    payload = decoded(response)
    return {
        "Cookie": response.headers["Set-Cookie"].split(";", 1)[0],
        "X-ARR-CSRF": payload["data"]["csrf_token"],
    }


class LoginCredentialTests(unittest.TestCase):
    def test_environment_configuration_is_required_and_validated(self) -> None:
        with self.assertRaisesRegex(ValueError, "requires ARR_WEB_USERNAME"):
            LoginCredentials.from_environment({})
        with self.assertRaisesRegex(ValueError, "12"):
            LoginCredentials.from_environment(
                {"ARR_WEB_USERNAME": USERNAME, "ARR_WEB_PASSWORD": "short"}
            )
        configured = LoginCredentials.from_environment(
            {"ARR_WEB_USERNAME": USERNAME, "ARR_WEB_PASSWORD": PASSWORD}
        )
        self.assertTrue(configured.verify(USERNAME, PASSWORD))
        self.assertFalse(configured.verify(USERNAME, "wrong-password-value"))
        self.assertFalse(configured.verify("other-operator", PASSWORD))


class PortalAuthenticationTests(unittest.TestCase):
    def setUp(self) -> None:
        self.app = PortalApplication(
            credentials=CREDENTIALS,
            health=RuntimeHealth(True, True, False),
        )

    def test_h5_shell_is_public_while_desktop_and_detail_routes_stay_private(self) -> None:
        desktop = self.app.handle("GET", "/", {})
        mobile = self.app.handle("GET", "/h5", {})
        mobile_css = self.app.handle("GET", "/assets/h5.css", {})
        mobile_js = self.app.handle("GET", "/assets/h5.js", {})
        protected_api = self.app.handle("GET", "/api/health", {})
        protected_asset = self.app.handle("GET", "/assets/app.js", {})
        login_page = self.app.handle("GET", "/login", {})
        login_css = self.app.handle("GET", "/assets/login.css", {})
        readiness = self.app.handle("GET", "/healthz", {})

        self.assertEqual((desktop.status, desktop.headers["Location"]), (303, "/login?next=%2F"))
        self.assertEqual(mobile.status, 200)
        self.assertEqual(mobile_css.status, 200)
        self.assertEqual(mobile_js.status, 200)
        self.assertEqual(protected_api.status, 401)
        self.assertEqual(decoded(protected_api)["error"]["code"], "AUTH_REQUIRED")
        self.assertEqual(protected_asset.status, 303)
        self.assertEqual(login_page.status, 200)
        self.assertIn('<h1 id="context-title">ARR Report</h1>', login_page.body.decode("utf-8"))
        self.assertEqual(login_css.status, 200)
        self.assertEqual((readiness.status, readiness.body), (200, b"ready\n"))

        unavailable = PortalApplication(
            credentials=CREDENTIALS,
            health=RuntimeHealth(True, False, False),
        ).handle("GET", "/healthz", {})
        self.assertEqual((unavailable.status, unavailable.body), (503, b"unavailable\n"))

    def test_login_is_strict_generic_and_issues_an_authenticated_session(self) -> None:
        wrong_media = self.app.handle(
            "POST",
            "/api/login",
            {"Content-Type": "application/x-www-form-urlencoded"},
            b"username=x&password=y",
        )
        wrong = login(self.app, password="incorrect-password-value")
        success = login(self.app)
        headers = auth_headers(success)
        session = self.app.handle("GET", "/api/session", headers)

        self.assertEqual(wrong_media.status, 415)
        self.assertEqual(wrong.status, 401)
        self.assertEqual(decoded(wrong)["error"]["code"], "LOGIN_FAILED")
        self.assertNotIn("incorrect-password-value", wrong.body.decode("utf-8"))
        self.assertNotIn("Set-Cookie", wrong.headers)
        self.assertEqual(success.status, 200)
        self.assertIn("HttpOnly", success.headers["Set-Cookie"])
        self.assertIn("SameSite=Strict", success.headers["Set-Cookie"])
        self.assertEqual(decoded(session)["data"]["username"], USERNAME)
        self.assertEqual(
            decoded(session)["data"]["csrf_token"],
            headers["X-ARR-CSRF"],
        )

    def test_authenticated_login_page_redirect_is_allowlisted(self) -> None:
        headers = auth_headers(login(self.app))
        mobile = self.app.handle("GET", "/login?next=/h5", headers)
        external = self.app.handle(
            "GET",
            "/login?next=https%3A%2F%2Fevil.example",
            headers,
        )
        duplicate = self.app.handle("GET", "/login?next=/h5&next=/", headers)

        self.assertEqual(mobile.headers["Location"], "/h5")
        self.assertEqual(external.headers["Location"], "/")
        self.assertEqual(duplicate.headers["Location"], "/")

    def test_logout_requires_csrf_revokes_session_and_expires_cookie(self) -> None:
        headers = auth_headers(login(self.app))
        no_csrf = self.app.handle(
            "POST",
            "/api/logout",
            {"Cookie": headers["Cookie"]},
        )
        logged_out = self.app.handle("POST", "/api/logout", headers)
        after = self.app.handle("GET", "/api/session", headers)

        self.assertEqual(no_csrf.status, 403)
        self.assertEqual(logged_out.status, 200)
        self.assertIn("Max-Age=0", logged_out.headers["Set-Cookie"])
        self.assertEqual(after.status, 401)

    def test_login_attempts_are_temporarily_rate_limited_per_client(self) -> None:
        now = [100.0]
        attempts = LoginAttemptLedger(
            max_failures=2,
            window_seconds=60,
            block_seconds=30,
            clock=lambda: now[0],
        )
        app = PortalApplication(credentials=CREDENTIALS, login_attempts=attempts)

        first = login(app, password="incorrect-password-value", client_id="client-a")
        second = login(app, password="incorrect-password-value", client_id="client-a")
        blocked_correct = login(app, client_id="client-a")
        other_client = login(app, client_id="client-b")
        now[0] += 31
        recovered = login(app, client_id="client-a")

        self.assertEqual(first.status, 401)
        self.assertEqual(second.status, 429)
        self.assertEqual(second.headers["Retry-After"], "30")
        self.assertEqual(blocked_correct.status, 429)
        self.assertEqual(other_client.status, 200)
        self.assertEqual(recovered.status, 200)


class StaticLoginContractTests(unittest.TestCase):
    def test_login_and_logout_assets_keep_accessibility_and_session_contracts(self) -> None:
        login_html = (STATIC_ROOT / "login.html").read_text(encoding="utf-8")
        login_css = (STATIC_ROOT / "login.css").read_text(encoding="utf-8")
        login_js = (STATIC_ROOT / "login.js").read_text(encoding="utf-8")
        desktop_html = (STATIC_ROOT / "index.html").read_text(encoding="utf-8")
        desktop_js = (STATIC_ROOT / "app.js").read_text(encoding="utf-8")
        mobile_html = (STATIC_ROOT / "h5.html").read_text(encoding="utf-8")
        mobile_js = (STATIC_ROOT / "h5.js").read_text(encoding="utf-8")

        for expected in (
            '<label for="username">username</label>',
            'autocomplete="username"',
            '<label for="password">password</label>',
            'autocomplete="current-password"',
            'id="password-toggle"',
            'aria-live="assertive"',
        ):
            self.assertIn(expected, login_html)
        for removed in (
            "OPERA ARR WORKFLOW",
            "让每一次到店数据",
            "上传 XML",
            "AUTHORIZED ACCESS",
            "欢迎回来",
            "无法登录？",
            "仅限获得授权的 Finance 操作人员使用",
        ):
            self.assertNotIn(removed, login_html)
        self.assertIn("@media (prefers-reduced-motion: reduce)", login_css)
        self.assertIn('candidate === "/h5" ? "/h5" : "/"', login_js)
        self.assertIn('id="logout-button"', desktop_html)
        self.assertIn('id="h5-logout"', mobile_html)
        self.assertIn('<a class="logout-link" id="logout-button" href="#logout" hidden>', desktop_html)
        self.assertIn('<a class="mobile-logout" id="h5-logout" href="#logout" hidden>', mobile_html)
        self.assertIn("if (!session.username) return", desktop_js)
        self.assertIn("if (session?.username)", mobile_js)
        self.assertIn("response.status === 401", desktop_js)
        self.assertIn('api("/api/logout", { method: "POST" })', desktop_js)
        self.assertIn("response.status === 401", mobile_js)
        self.assertIn('api("/api/logout", { method: "POST" })', mobile_js)
        self.assertIn("/api/public/h5/months", mobile_js)
        self.assertIn("/api/public/h5/analytics", mobile_js)
        self.assertIn('fetch("/healthz"', mobile_js)


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