from datetime import timedelta
from unittest.mock import patch

import httpx
import pytest
from fastapi.testclient import TestClient

from sitedrop.client.api import SitedropClient, SitedropError, _validate_url
from sitedrop.client.config import ClientConfig
from sitedrop.server.app import create_app
from sitedrop.server.auth import create_token, hash_password
from sitedrop.server.config import ServerConfig


@pytest.fixture
def server_config(tmp_path):
    return ServerConfig(
        password_hash=hash_password("testpass"),
        jwt_secret="test-secret-key-long-enough-for-jwt",
        sites_dir=str(tmp_path / "sites"),
    )


@pytest.fixture
def app(server_config):
    return create_app(server_config)


@pytest.fixture
def test_client(app):
    return TestClient(app)


@pytest.fixture
def patched_httpx(test_client):
    """Patch httpx.post/get/put/delete to route through TestClient."""

    def _request(method, url, **kwargs):
        kwargs.pop("timeout", None)
        path = url.split("://", 1)[1]
        path = "/" + path.split("/", 1)[1] if "/" in path else "/"
        tc_method = getattr(test_client, method)
        return tc_method(path, **kwargs)

    def post(url, **kwargs):
        return _request("post", url, **kwargs)

    def get(url, **kwargs):
        return _request("get", url, **kwargs)

    def put(url, **kwargs):
        return _request("put", url, **kwargs)

    def delete(url, **kwargs):
        return _request("delete", url, **kwargs)

    with (
        patch.object(httpx, "post", side_effect=post),
        patch.object(httpx, "get", side_effect=get),
        patch.object(httpx, "put", side_effect=put),
        patch.object(httpx, "delete", side_effect=delete),
    ):
        yield


@pytest.fixture
def client_config(tmp_path, patched_httpx):
    config = ClientConfig(
        server_url="http://testserver",
        password="testpass",
    )
    config.save(tmp_path / ".sitedroprc")
    return config


@pytest.fixture
def api_client(client_config):
    return SitedropClient(client_config)


def test_login(tmp_path, patched_httpx):
    config = ClientConfig()
    config.save(tmp_path / ".sitedroprc")
    client = SitedropClient(config)
    token = client.login("http://testserver", "testpass")
    assert token
    assert client.config.server_url == "http://testserver"
    assert client.config.password == "testpass"
    assert client.config.token == token


def test_login_wrong_password(tmp_path, patched_httpx):
    config = ClientConfig()
    config.save(tmp_path / ".sitedroprc")
    client = SitedropClient(config)
    with pytest.raises(SitedropError, match="Authentication failed"):
        client.login("http://testserver", "wrong")


def test_upload_and_list(api_client):
    result = api_client.upload("mysite", "<h1>Hello</h1>")
    assert result["name"] == "mysite"
    assert result["size"] > 0

    sites = api_client.list_sites()
    assert len(sites) == 1
    assert sites[0]["name"] == "mysite"


def test_upload_and_delete(api_client):
    api_client.upload("todelete", "<p>bye</p>")
    msg = api_client.delete("todelete")
    assert "todelete" in msg.lower() or "Deleted" in msg

    sites = api_client.list_sites()
    assert len(sites) == 0


def test_delete_nonexistent(api_client):
    with pytest.raises(SitedropError, match="Not found"):
        api_client.delete("nope")


def test_list_empty(api_client):
    sites = api_client.list_sites()
    assert sites == []


def test_token_auto_refresh(api_client, server_config):
    expired = create_token(
        server_config.jwt_secret, expires_delta=timedelta(seconds=-1)
    )
    api_client.config.token = expired

    sites = api_client.list_sites()
    assert sites == []
    assert api_client.config.token != expired


def test_token_expired_no_password(server_config):
    config = ClientConfig(
        server_url="http://localhost:1",
        token="",
        password="",
    )
    client = SitedropClient(config)
    with pytest.raises(RuntimeError, match="No password saved"):
        client._ensure_token()


def test_upload_overwrite_with_force(api_client):
    api_client.upload("page", "<p>v1</p>", force=True)
    api_client.upload("page", "<p>v2</p>", force=True)
    sites = api_client.list_sites()
    assert len(sites) == 1


def test_upload_default_rejects_existing(api_client):
    api_client.upload("page", "<p>v1</p>")
    with pytest.raises(SitedropError, match="already exists"):
        api_client.upload("page", "<p>v2</p>")


# --- URL validation tests ---


def test_validate_url_http():
    assert _validate_url("http://example.com") == "http://example.com"


def test_validate_url_https():
    assert _validate_url("https://example.com/") == "https://example.com"


def test_validate_url_no_scheme():
    with pytest.raises(SitedropError, match="Must start with http"):
        _validate_url("example.com")


def test_validate_url_file_scheme():
    with pytest.raises(SitedropError, match="Must start with http"):
        _validate_url("file:///etc/passwd")


def test_validate_url_ftp_scheme():
    with pytest.raises(SitedropError, match="Must start with http"):
        _validate_url("ftp://example.com")


def test_validate_url_no_host():
    with pytest.raises(SitedropError, match="No hostname"):
        _validate_url("http://")


# --- Network error tests ---


def test_connection_refused():
    config = ClientConfig(
        server_url="http://127.0.0.1:1",
        password="pass",
    )
    client = SitedropClient(config)
    client.config.token = "fake"
    with pytest.raises(SitedropError, match="Cannot connect"):
        client.list_sites()


def test_login_connection_refused():
    config = ClientConfig()
    client = SitedropClient(config)
    with pytest.raises(SitedropError, match="Cannot connect"):
        client.login("http://127.0.0.1:1", "pass")


# --- change_password tests ---


def test_change_password(api_client):
    result = api_client.change_password("testpass", "newpass123")
    assert isinstance(result, dict)
    assert "updated" in result["message"].lower()
    assert "token" in result
    assert api_client.config.token == result["token"]


def test_change_password_wrong_current(api_client):
    with pytest.raises(SitedropError, match="Authentication failed"):
        api_client.change_password("wrongpass", "newpass123")
