import os
import sys
from collections.abc import Generator
from pathlib import Path
from unittest.mock import AsyncMock, patch

import pytest
from fastapi.testclient import TestClient

from pygitweb.auth import ACCESS_TOKEN_COOKIE_NAME
from pygitweb.auth_config import AuthConfig, LocalUser
from pygitweb.auth_oauth import OAUTH_STATE_COOKIE_NAME, _seal_oauth_state
from pygitweb.config import settings
from pygitweb.conftest import clear_client_cookies
from pygitweb.gravatar import gravatar_url
from pygitweb.main import app
from pygitweb.permissions import PERMISSION_ADD_PROJECTS, PermissionsMap
from pygitweb.sessions import get_session

_LOCAL_ADMIN = AuthConfig(
	auth_mode="local",
	local_users=[LocalUser(user="admin", password="secret")],
)


@pytest.fixture
def client() -> Generator[TestClient, None, None]:
	with TestClient(app) as c:
		yield c


@pytest.fixture
def local_auth_config() -> Generator[AuthConfig, None, None]:
	with (
		patch("pygitweb.auth_config.auth_config", _LOCAL_ADMIN),
		patch("pygitweb.auth.auth_config", _LOCAL_ADMIN),
		patch("pygitweb.main.auth_config", _LOCAL_ADMIN),
	):
		yield _LOCAL_ADMIN


def test_token_disabled(client: TestClient) -> None:
	with patch.object(settings, "AUTH", False):
		r = client.post("/token", data={"username": "a", "password": "b"})
		assert r.status_code == 400
		assert r.json()["detail"] == "Authentication is disabled"


def test_token_success(client: TestClient, local_auth_config: AuthConfig) -> None:
	with patch.object(settings, "AUTH", True):
		r = client.post("/token", data={"username": "admin", "password": "secret"})
	assert r.status_code == 200
	data = r.json()
	assert data["token_type"] == "bearer"
	assert isinstance(data["access_token"], str)
	assert len(data["access_token"]) >= 32
	assert data["access_token"] != "admin"


def test_token_wrong_password(client: TestClient, local_auth_config: AuthConfig) -> None:
	with patch.object(settings, "AUTH", True):
		r = client.post("/token", data={"username": "admin", "password": "wrong"})
	assert r.status_code == 400


def test_users_me_with_bearer(client: TestClient, local_auth_config: AuthConfig) -> None:
	with patch.object(settings, "AUTH", True):
		tok = client.post("/token", data={"username": "admin", "password": "secret"}).json()["access_token"]
		r = client.get("/users/me", headers={"Authorization": f"Bearer {tok}"})
		assert r.status_code == 200
		assert r.json()["username"] == "admin"


def test_login_form_sets_cookie_and_users_me(client: TestClient, local_auth_config: AuthConfig) -> None:
	with patch.object(settings, "AUTH", True):
		r = client.post(
			"/login",
			data={"username": "admin", "password": "secret", "next": "/"},
			follow_redirects=False,
		)
		assert r.status_code == 303
		r2 = client.get("/users/me")
		assert r2.status_code == 200
		assert r2.json()["username"] == "admin"


def test_logout_revokes_cookie_session(client: TestClient, local_auth_config: AuthConfig) -> None:
	with patch.object(settings, "AUTH", True):
		client.post(
			"/login",
			data={"username": "admin", "password": "secret", "next": "/"},
			follow_redirects=False,
		)
		assert client.get("/users/me").status_code == 200
		r_out = client.get("/logout", params={"next": "/"}, follow_redirects=False)
		assert r_out.status_code == 303
		assert client.get("/users/me").status_code == 401


def test_logout_with_bearer_revokes_token(client: TestClient, local_auth_config: AuthConfig) -> None:
	with patch.object(settings, "AUTH", True):
		tok = client.post("/token", data={"username": "admin", "password": "secret"}).json()["access_token"]
		headers = {"Authorization": f"Bearer {tok}"}
		assert client.get("/users/me", headers=headers).status_code == 200
		r_out = client.get("/logout", params={"next": "/"}, headers=headers, follow_redirects=False)
		assert r_out.status_code == 303
		assert client.get("/users/me", headers=headers).status_code == 401


def test_projectnamevalid_no_auth_required_when_auth_disabled(client: TestClient) -> None:
	with patch.object(settings, "AUTH", False), patch("pygitweb.main.project_visible_in_list", return_value=False):
		r = client.get("/projectnamevalid", params={"name": "newproj"})
	assert r.status_code == 200


def test_projectnamevalid_401_without_credentials_when_auth_enabled(
	client: TestClient, local_auth_config: AuthConfig
) -> None:
	with patch.object(settings, "AUTH", True):
		r = client.get("/projectnamevalid", params={"name": "newproj"})
	assert r.status_code == 401


def test_projectnamevalid_ok_with_bearer_when_auth_enabled(client: TestClient, local_auth_config: AuthConfig) -> None:
	cfg = local_auth_config.model_copy(
		update={
			"oauth_permissions": PermissionsMap.model_validate({
				"*": [],
				PERMISSION_ADD_PROJECTS: ["admin"],
			}),
		},
	)
	with (
		patch.object(settings, "AUTH", True),
		patch("pygitweb.main.project_visible_in_list", return_value=False),
		patch("pygitweb.auth_config.auth_config", cfg),
		patch("pygitweb.auth.auth_config", cfg),
		patch("pygitweb.main.auth_config", cfg),
	):
		tok = client.post("/token", data={"username": "admin", "password": "secret"}).json()["access_token"]
		r = client.get(
			"/projectnamevalid",
			params={"name": "newproj"},
			headers={"Authorization": f"Bearer {tok}"},
		)
	assert r.status_code == 200


def test_auth_config_init_creates_file_only_when_auth_enabled(tmp_path: Path) -> None:
	from pygitweb.auth_config import init_auth_config
	from pygitweb.config import Settings

	cfg_path = tmp_path / "auth.json"
	s = Settings(AUTH=True, AUTH_CONFIG=str(cfg_path))
	assert not cfg_path.is_file()
	cfg = init_auth_config(s)
	assert cfg_path.is_file()
	assert cfg.local_users[0].user == "admin"
	assert len(cfg.local_users[0].password) >= 8
	if sys.platform != "win32":
		assert oct(os.stat(cfg_path).st_mode & 0o777) == "0o600"


def test_auth_config_no_file_when_auth_disabled(tmp_path: Path) -> None:
	from pygitweb.auth_config import init_auth_config
	from pygitweb.config import Settings

	cfg_path = tmp_path / "auth.json"
	s = Settings(AUTH=False, AUTH_CONFIG=str(cfg_path))
	init_auth_config(s)
	assert not cfg_path.is_file()


def test_auth_config_does_not_regenerate_missing_password(tmp_path: Path) -> None:
	from pygitweb.auth_config import init_auth_config, write_auth_config
	from pygitweb.config import Settings

	cfg_path = tmp_path / "auth.json"
	write_auth_config(
		cfg_path,
		AuthConfig(auth_mode="local", local_users=[LocalUser(user="admin", password="")]),
	)
	s = Settings(AUTH=True, AUTH_CONFIG=str(cfg_path))
	cfg = init_auth_config(s)
	assert cfg.local_users[0].password == ""


@pytest.mark.skipif(sys.platform == "win32", reason="Unix file modes")
def test_auth_config_rejects_insecure_permissions(tmp_path: Path) -> None:
	from pygitweb.auth_config import AuthConfigPermissionError, init_auth_config, write_auth_config
	from pygitweb.config import Settings

	cfg_path = tmp_path / "auth.json"
	write_auth_config(
		cfg_path,
		AuthConfig(auth_mode="local", local_users=[LocalUser(user="admin", password="secret")]),
	)
	os.chmod(cfg_path, 0o644)
	s = Settings(AUTH=True, AUTH_CONFIG=str(cfg_path))
	with pytest.raises(AuthConfigPermissionError, match="0600"):
		init_auth_config(s)


def test_local_login_stores_session_audit_fields(client: TestClient, local_auth_config: AuthConfig) -> None:
	with patch.object(settings, "AUTH", True):
		client.post(
			"/login",
			data={"username": "admin", "password": "secret", "next": "/"},
			follow_redirects=False,
		)
		tok = client.post("/token", data={"username": "admin", "password": "secret"}).json()["access_token"]
	rec = get_session(tok)
	assert rec is not None
	assert rec.username == "admin"
	assert rec.auth_method == "local"
	assert rec.subject is None
	assert rec.email is None


_OAUTH_CFG = AuthConfig(
	auth_mode="oauth",
	oauth_provider="google",
	oauth_client_id="test-client-id",
	oauth_client_secret="test-client-secret",
	oauth_redirect_uri="http://testserver/auth/oauth/callback",
	oauth_allowed_emails=[],
	local_users=[],
)


@pytest.fixture
def oauth_auth_config() -> Generator[AuthConfig, None, None]:
	with (
		patch("pygitweb.auth_config.auth_config", _OAUTH_CFG),
		patch("pygitweb.auth.auth_config", _OAUTH_CFG),
		patch("pygitweb.auth_oauth.auth_config", _OAUTH_CFG),
		patch("pygitweb.main.auth_config", _OAUTH_CFG),
	):
		yield _OAUTH_CFG


def test_oauth_start_redirects_to_provider(client: TestClient, oauth_auth_config: AuthConfig) -> None:
	with patch.object(settings, "AUTH", True):
		r = client.get("/auth/oauth/start", params={"next": "/boards"}, follow_redirects=False)
	assert r.status_code == 303
	assert "accounts.google.com" in r.headers["location"]
	assert OAUTH_STATE_COOKIE_NAME in r.cookies


def test_oauth_callback_creates_session(client: TestClient, oauth_auth_config: AuthConfig) -> None:
	sealed = _seal_oauth_state(
		{"state": "idp-state", "next": "/", "code_verifier": "verifier"},
		oauth_auth_config.oauth_client_secret,
	)
	token_payload = {"access_token": "provider-token"}
	profile = {"sub": "oauth-sub-1", "email": "user@example.com", "preferred_username": "oauthuser"}
	with (
		patch.object(settings, "AUTH", True),
		patch("pygitweb.auth_oauth._exchange_code", new_callable=AsyncMock, return_value=token_payload),
		patch("pygitweb.auth_oauth._fetch_profile", new_callable=AsyncMock, return_value=profile),
	):
		client.cookies.set(OAUTH_STATE_COOKIE_NAME, sealed)
		r = client.get(
			"/auth/oauth/callback",
			params={"code": "auth-code", "state": "idp-state"},
			follow_redirects=False,
		)
	assert r.status_code == 303
	rec = get_session(r.cookies["pygitweb_access_token"])
	assert rec is not None
	assert rec.username == "user@example.com"
	assert rec.subject == "oauth-sub-1"
	assert rec.email == "user@example.com"
	assert rec.auth_method == "oauth"
	assert client.get("/users/me").json()["username"] == "user@example.com"
	status = client.get("/auth/status").json()
	assert status["username"] == "user@example.com"
	assert status["gravatar_url"] == gravatar_url("user@example.com")
	clear_client_cookies(client)


def test_auth_status_no_gravatar_for_local_login(client: TestClient, local_auth_config: AuthConfig) -> None:
	with patch.object(settings, "AUTH", True):
		client.post(
			"/login",
			data={"username": "admin", "password": "secret", "next": "/"},
			follow_redirects=False,
		)
		status = client.get("/auth/status").json()
	assert status["username"] == "admin"
	assert status["gravatar_url"] is None


def test_local_user_email_gravatar(client: TestClient) -> None:
	cfg = AuthConfig(
		auth_mode="local",
		local_users=[LocalUser(user="admin", password="secret", email="  admin@example.com  ")],
	)
	with (
		patch.object(settings, "AUTH", True),
		patch("pygitweb.auth_config.auth_config", cfg),
		patch("pygitweb.auth.auth_config", cfg),
		patch("pygitweb.main.auth_config", cfg),
	):
		client.post(
			"/login",
			data={"username": "admin", "password": "secret", "next": "/"},
			follow_redirects=False,
		)
		status = client.get("/auth/status").json()
	assert status["gravatar_url"] == gravatar_url("admin@example.com")
	rec = get_session(client.cookies[ACCESS_TOKEN_COOKIE_NAME])
	assert rec is not None
	assert rec.email == "admin@example.com"


def test_authenticate_local_user_disabled_in_oauth_only_mode() -> None:
	from pygitweb.auth import authenticate_local_user

	with patch("pygitweb.auth.auth_config", _OAUTH_CFG):
		assert authenticate_local_user("admin", "secret") is None