"""Tests for waft_client."""

import json
import time

from waft_client import AuthConfig, WaftAuth
from waft_client.providers.password import hash_password, verify_password
from waft_client.session import create_session_token, create_token, verify_token
from waft_client.testing import make_test_event
from waft_client.testing import test_auth_header as build_test_auth_header


# --- Session / JWT tests ---


class TestSession:
    def test_create_and_verify_token(self):
        secret = "test-secret"
        payload = {"sub": "user-1", "email": "a@b.com", "exp": int(time.time()) + 3600}
        token = create_token(payload, secret)
        result = verify_token(token, secret)
        assert result is not None
        assert result["sub"] == "user-1"
        assert result["email"] == "a@b.com"

    def test_expired_token_returns_none(self):
        secret = "test-secret"
        payload = {"sub": "user-1", "exp": int(time.time()) - 10}
        token = create_token(payload, secret)
        assert verify_token(token, secret) is None

    def test_wrong_secret_returns_none(self):
        payload = {"sub": "user-1", "exp": int(time.time()) + 3600}
        token = create_token(payload, "secret-a")
        assert verify_token(token, "secret-b") is None

    def test_invalid_token_returns_none(self):
        assert verify_token("not.a.jwt", "secret") is None
        assert verify_token("", "secret") is None

    def test_create_session_token(self):
        token = create_session_token("u1", "a@b.com", "secret", max_age=60)
        payload = verify_token(token, "secret")
        assert payload is not None
        assert payload["sub"] == "u1"
        assert payload["email"] == "a@b.com"


# --- Password provider tests ---


class TestPassword:
    def test_hash_and_verify(self):
        h = hash_password("my-password")
        assert verify_password("my-password", h)
        assert not verify_password("wrong-password", h)

    def test_different_salts(self):
        h1 = hash_password("same-password")
        h2 = hash_password("same-password")
        # Different salts produce different hashes
        assert h1 != h2
        # Both verify correctly
        assert verify_password("same-password", h1)
        assert verify_password("same-password", h2)

    def test_invalid_hash_returns_false(self):
        assert not verify_password("password", "not-a-valid-hash")
        assert not verify_password("password", "")


# --- Store tests ---


class TestStore:
    def test_create_and_get_user(self, store):
        user = store.create_user("alice@example.com")
        assert user.email == "alice@example.com"
        assert user.id is not None

        fetched = store.get_user(user.id)
        assert fetched is not None
        assert fetched.email == "alice@example.com"

    def test_get_user_by_email(self, store):
        store.create_user("bob@example.com")
        user = store.get_user_by_email("bob@example.com")
        assert user is not None
        assert user.email == "bob@example.com"

    def test_get_nonexistent_user(self, store):
        assert store.get_user("nonexistent") is None
        assert store.get_user_by_email("nobody@example.com") is None

    def test_list_users(self, store):
        store.create_user("one@example.com")
        store.create_user("two@example.com")
        users = store.list_users()
        assert len(users) == 2
        assert {u.email for u in users} == {"one@example.com", "two@example.com"}

    def test_credentials(self, store):
        user = store.create_user("cred@example.com")
        store.store_credential(user.id, "password", "cred@example.com", "hashed-pw")

        cred = store.get_credential(user.id, "password")
        assert cred is not None
        assert cred == ("cred@example.com", "hashed-pw")

        by_ext = store.get_credential_by_external("password", "cred@example.com")
        assert by_ext is not None
        assert by_ext[0] == user.id

    def test_get_user_by_provider(self, store):
        user = store.create_user("provider@example.com")
        store.store_credential(user.id, "google", "google-123", "")

        found = store.get_user_by_provider("google", "google-123")
        assert found is not None
        assert found.id == user.id

        assert store.get_user_by_provider("google", "nonexistent") is None

    def test_credential_upsert(self, store):
        user = store.create_user("upsert@example.com")
        store.store_credential(user.id, "password", "upsert@example.com", "hash1")
        store.store_credential(user.id, "password", "upsert@example.com", "hash2")

        cred = store.get_credential(user.id, "password")
        assert cred is not None
        assert cred[1] == "hash2"

    def test_invites(self, store):
        assert not store.has_invite("invite@example.com")
        token = store.create_invite("invite@example.com")
        assert token is not None
        assert store.has_invite("invite@example.com")
        store.use_invite("invite@example.com")
        assert not store.has_invite("invite@example.com")


# --- WaftAuth integration tests ---


class TestWaftAuth:
    def test_login_page(self, auth):
        event = {"rawPath": "/auth/login", "requestContext": {"http": {"method": "GET"}}}
        response = auth.handle(event)
        assert response is not None
        assert response["statusCode"] == 200
        assert "Sign In" in response["body"]

    def test_register_page(self, auth):
        event = {"rawPath": "/auth/register", "requestContext": {"http": {"method": "GET"}}}
        response = auth.handle(event)
        assert response is not None
        assert response["statusCode"] == 200
        assert "Create Account" in response["body"]

    def test_register_blocked_in_invite_only(self, auth):
        auth.config.signup_mode = "invite_only"
        event = {"rawPath": "/auth/register", "requestContext": {"http": {"method": "GET"}}}
        response = auth.handle(event)
        assert response is not None
        assert "invitation" in response["body"].lower()

    def test_providers_json(self, auth):
        event = {"rawPath": "/auth/providers", "requestContext": {"http": {"method": "GET"}}}
        response = auth.handle(event)
        assert response is not None
        assert response["statusCode"] == 200
        providers = json.loads(response["body"])
        ids = {p["id"] for p in providers}
        assert "google" in ids
        assert "password" in ids

    def test_non_auth_route_returns_none(self, auth):
        event = {"rawPath": "/", "requestContext": {"http": {"method": "GET"}}}
        assert auth.handle(event) is None

    def test_logout_clears_cookie(self, auth):
        event = {"rawPath": "/auth/logout", "requestContext": {"http": {"method": "GET"}}}
        response = auth.handle(event)
        assert response is not None
        assert response["statusCode"] == 302
        assert any("Max-Age=0" in c for c in response.get("cookies", []))

    def test_get_user_test_mode(self, auth):
        event = make_test_event(user_id="test-123", email="test@test.com")
        user = auth.get_user(event)
        assert user is not None
        assert user.id == "test-123"
        assert user.email == "test@test.com"

    def test_get_user_no_auth(self, auth):
        event = {"rawPath": "/", "headers": {}, "cookies": []}
        user = auth.get_user(event)
        assert user is None

    def test_get_user_via_cookie(self, auth, store):
        # Create a user and a session token
        user = store.create_user("cookie@example.com")
        token = create_session_token(
            user.id, user.email, auth.config.jwt_secret, 3600
        )
        event = {
            "rawPath": "/",
            "headers": {},
            "cookies": [f"waft_auth={token}"],
        }
        result = auth.get_user(event)
        assert result is not None
        assert result.id == user.id

    def test_get_user_via_cookie_header(self, auth, store):
        user = store.create_user("header@example.com")
        token = create_session_token(
            user.id, user.email, auth.config.jwt_secret, 3600
        )
        event = {
            "rawPath": "/",
            "headers": {"cookie": f"waft_auth={token}"},
            "cookies": [],
        }
        result = auth.get_user(event)
        assert result is not None
        assert result.id == user.id

    def test_login_redirect(self, auth):
        resp = auth.login_redirect()
        assert resp["statusCode"] == 302
        assert resp["headers"]["location"] == "/auth/login"

    def test_invite_user(self, auth, store):
        token = auth.invite_user("invited@example.com")
        assert token is not None
        assert store.has_invite("invited@example.com")

    def test_me_authenticated(self, auth, store):
        user = store.create_user("me@example.com")
        token = create_session_token(user.id, user.email, auth.config.jwt_secret, 3600)
        event = {
            "rawPath": "/auth/me",
            "requestContext": {"http": {"method": "GET"}},
            "headers": {},
            "cookies": [f"waft_auth={token}"],
        }
        response = auth.handle(event)
        assert response["statusCode"] == 200
        data = json.loads(response["body"])
        assert data["email"] == "me@example.com"

    def test_me_unauthenticated(self, auth):
        event = {
            "rawPath": "/auth/me",
            "requestContext": {"http": {"method": "GET"}},
            "headers": {},
            "cookies": [],
        }
        response = auth.handle(event)
        assert response["statusCode"] == 401


# --- Password auth flow tests ---


class TestPasswordFlow:
    def _register(self, auth, email="user@example.com", password="securepassword"):
        from urllib.parse import urlencode

        body = urlencode({"email": email, "password": password})
        return auth.handle({
            "rawPath": "/auth/password/register",
            "requestContext": {"http": {"method": "POST"}},
            "headers": {},
            "cookies": [],
            "body": body,
            "isBase64Encoded": False,
        })

    def _login(self, auth, email="user@example.com", password="securepassword"):
        from urllib.parse import urlencode

        body = urlencode({"email": email, "password": password})
        return auth.handle({
            "rawPath": "/auth/password/login",
            "requestContext": {"http": {"method": "POST"}},
            "headers": {},
            "cookies": [],
            "body": body,
            "isBase64Encoded": False,
        })

    def test_register_and_login(self, auth, store):
        # Register
        response = self._register(auth)
        assert response["statusCode"] == 302
        assert response["headers"]["location"] == "/"
        assert any("waft_auth=" in c for c in response.get("cookies", []))

        # Verify user was created
        user = store.get_user_by_email("user@example.com")
        assert user is not None

        # Login
        response = self._login(auth)
        assert response["statusCode"] == 302

    def test_login_wrong_password(self, auth):
        self._register(auth)
        response = self._login(auth, password="wrongpassword")
        assert response["statusCode"] == 200
        assert "Invalid" in response["body"]

    def test_login_nonexistent_user(self, auth):
        response = self._login(auth, email="nobody@example.com")
        assert response["statusCode"] == 200
        assert "Invalid" in response["body"]

    def test_register_duplicate_email(self, auth):
        self._register(auth)
        response = self._register(auth)
        assert response["statusCode"] == 200
        assert "already exists" in response["body"]

    def test_register_short_password(self, auth):
        response = self._register(auth, password="short")
        assert response["statusCode"] == 200
        assert "8 characters" in response["body"]

    def test_register_invite_only(self, auth, store):
        auth.config.signup_mode = "invite_only"

        # Try to register without invite
        response = self._register(auth)
        assert response["statusCode"] == 200
        assert "invitation" in response["body"].lower()

        # Invite the user
        auth.invite_user("user@example.com")

        # Now registration works
        response = self._register(auth)
        assert response["statusCode"] == 302

        # Invite is consumed
        assert not store.has_invite("user@example.com")

    def test_register_empty_fields(self, auth):
        response = self._register(auth, email="", password="")
        assert response["statusCode"] == 200
        assert "required" in response["body"].lower()


# --- OAuth callback tests ---


class TestOAuthCallback:
    def test_successful_callback(self, auth, store):
        # Simulate an OAuth callback from the waft auth service
        token_payload = {
            "provider": "google",
            "email": "oauth@example.com",
            "external_id": "google-user-123",
            "app_id": "test-app",
            "exp": int(time.time()) + 300,
        }
        callback_token = create_token(token_payload, auth.config.app_secret)

        event = {
            "rawPath": "/auth/callback",
            "requestContext": {"http": {"method": "GET"}},
            "headers": {},
            "cookies": [],
            "queryStringParameters": {"token": callback_token},
        }
        response = auth.handle(event)
        assert response["statusCode"] == 302
        assert response["headers"]["location"] == "/"
        assert any("waft_auth=" in c for c in response.get("cookies", []))

        # User was created
        user = store.get_user_by_email("oauth@example.com")
        assert user is not None

        # Credential was stored
        found = store.get_user_by_provider("google", "google-user-123")
        assert found is not None
        assert found.id == user.id

    def test_callback_links_existing_user(self, auth, store):
        # Create a user with password first
        existing = store.create_user("linked@example.com")

        # OAuth callback with same email links to existing user
        token_payload = {
            "provider": "google",
            "email": "linked@example.com",
            "external_id": "google-linked-456",
            "app_id": "test-app",
            "exp": int(time.time()) + 300,
        }
        callback_token = create_token(token_payload, auth.config.app_secret)

        event = {
            "rawPath": "/auth/callback",
            "requestContext": {"http": {"method": "GET"}},
            "headers": {},
            "cookies": [],
            "queryStringParameters": {"token": callback_token},
        }
        response = auth.handle(event)
        assert response["statusCode"] == 302

        # Same user, now with google credential linked
        found = store.get_user_by_provider("google", "google-linked-456")
        assert found is not None
        assert found.id == existing.id

    def test_callback_invalid_token(self, auth):
        event = {
            "rawPath": "/auth/callback",
            "requestContext": {"http": {"method": "GET"}},
            "headers": {},
            "cookies": [],
            "queryStringParameters": {"token": "invalid-token"},
        }
        response = auth.handle(event)
        assert response["statusCode"] == 200
        assert "Error" in response["body"]

    def test_callback_invite_only(self, auth, store):
        auth.config.signup_mode = "invite_only"

        token_payload = {
            "provider": "google",
            "email": "uninvited@example.com",
            "external_id": "google-uninvited",
            "app_id": "test-app",
            "exp": int(time.time()) + 300,
        }
        callback_token = create_token(token_payload, auth.config.app_secret)

        event = {
            "rawPath": "/auth/callback",
            "requestContext": {"http": {"method": "GET"}},
            "headers": {},
            "cookies": [],
            "queryStringParameters": {"token": callback_token},
        }
        response = auth.handle(event)
        assert "invitation" in response["body"].lower()

        # Invite the user and try again
        store.create_invite("uninvited@example.com")
        response = auth.handle(event)
        assert response["statusCode"] == 302


# --- Testing utilities tests ---


class TestTestingUtils:
    def test_test_auth_header(self):
        headers = build_test_auth_header("u1", "u1@test.com")
        assert "X-Waft-Test-Auth" in headers
        data = json.loads(headers["X-Waft-Test-Auth"])
        assert data["user_id"] == "u1"
        assert data["email"] == "u1@test.com"

    def test_make_test_event(self):
        event = make_test_event(path="/api/data", user_id="u2")
        assert event["rawPath"] == "/api/data"
        assert "x-waft-test-auth" in event["headers"]

    def test_test_mode_required(self, store):
        """Without test_mode=True, the test header is ignored."""
        config = AuthConfig(
            jwt_secret="test",
            test_mode=False,  # test mode OFF
        )
        auth = WaftAuth(config=config, store=store)
        event = make_test_event(user_id="test-user")
        user = auth.get_user(event)
        assert user is None  # header ignored
