def test_auth_success(client):
    resp = client.post("/api/auth", json={"password": "testpass"})
    assert resp.status_code == 200
    assert "token" in resp.json()


def test_auth_wrong_password(client):
    resp = client.post("/api/auth", json={"password": "wrong"})
    assert resp.status_code == 401


def test_list_sites_empty(client, auth_headers):
    resp = client.get("/api/sites", headers=auth_headers)
    assert resp.status_code == 200
    assert resp.json()["sites"] == []


def test_list_sites_unauthorized(client):
    resp = client.get("/api/sites")
    assert resp.status_code in (401, 403)


def test_put_and_list(client, auth_headers):
    resp = client.put(
        "/api/sites/hello",
        content=b"<h1>Hello</h1>",
        headers=auth_headers,
    )
    assert resp.status_code == 200
    data = resp.json()
    assert data["name"] == "hello"
    assert data["size"] > 0

    resp = client.get("/api/sites", headers=auth_headers)
    names = [s["name"] for s in resp.json()["sites"]]
    assert "hello" in names


def test_put_invalid_name(client, auth_headers):
    resp = client.put(
        "/api/sites/.evil",
        content=b"bad",
        headers=auth_headers,
    )
    assert resp.status_code == 400


def test_delete_site(client, auth_headers):
    client.put("/api/sites/todelete", content=b"<p>bye</p>", headers=auth_headers)
    resp = client.delete("/api/sites/todelete", headers=auth_headers)
    assert resp.status_code == 200

    resp = client.delete("/api/sites/todelete", headers=auth_headers)
    assert resp.status_code == 404


def test_put_unauthorized(client):
    resp = client.put("/api/sites/hello", content=b"<h1>Hi</h1>")
    assert resp.status_code in (401, 403)


def test_delete_unauthorized(client):
    resp = client.delete("/api/sites/hello")
    assert resp.status_code in (401, 403)


def test_put_overwrite(client, auth_headers):
    client.put("/api/sites/page", content=b"<p>v1</p>", headers=auth_headers)
    resp = client.put("/api/sites/page", content=b"<p>v2</p>", headers=auth_headers)
    assert resp.status_code == 200
    assert resp.json()["name"] == "page"


def test_put_if_none_match_rejects_existing(client, auth_headers):
    client.put("/api/sites/page", content=b"<p>v1</p>", headers=auth_headers)
    headers = {**auth_headers, "If-None-Match": "*"}
    resp = client.put("/api/sites/page", content=b"<p>v2</p>", headers=headers)
    assert resp.status_code == 412
    assert "already exists" in resp.json()["detail"]


def test_put_if_none_match_allows_new(client, auth_headers):
    headers = {**auth_headers, "If-None-Match": "*"}
    resp = client.put("/api/sites/newsite", content=b"<p>hi</p>", headers=headers)
    assert resp.status_code == 200
    assert resp.json()["name"] == "newsite"


def test_auth_unconfigured_server(tmp_path):
    from fastapi.testclient import TestClient
    from sitedrop.server.app import create_app
    from sitedrop.server.config import ServerConfig

    config = ServerConfig(password_hash="", sites_dir=str(tmp_path / "sites"))
    app = create_app(config)
    c = TestClient(app)
    resp = c.post("/api/auth", json={"password": "anything"})
    assert resp.status_code == 500


def test_change_password(client, auth_headers):
    resp = client.post(
        "/api/password",
        json={"current_password": "testpass", "new_password": "newpass"},
        headers=auth_headers,
    )
    assert resp.status_code == 200
    data = resp.json()
    assert "token" in data
    assert data["message"] == "Password updated"
    new_token = data["token"]

    # Old token should be rejected (JWT secret was rotated)
    resp = client.get("/api/sites", headers=auth_headers)
    assert resp.status_code == 401

    # New token should work
    new_headers = {"Authorization": f"Bearer {new_token}"}
    resp = client.get("/api/sites", headers=new_headers)
    assert resp.status_code == 200

    # Old password should fail
    resp = client.post("/api/auth", json={"password": "testpass"})
    assert resp.status_code == 401

    # New password should work
    resp = client.post("/api/auth", json={"password": "newpass"})
    assert resp.status_code == 200


def test_change_password_wrong_current(client, auth_headers):
    resp = client.post(
        "/api/password",
        json={"current_password": "wrong", "new_password": "newpass"},
        headers=auth_headers,
    )
    assert resp.status_code == 401


def test_change_password_unauthorized(client):
    resp = client.post(
        "/api/password",
        json={"current_password": "testpass", "new_password": "newpass"},
    )
    assert resp.status_code in (401, 403)


def test_delete_invalid_name(client, auth_headers):
    resp = client.delete("/api/sites/.bad", headers=auth_headers)
    assert resp.status_code == 400


def test_put_non_utf8(client, auth_headers):
    resp = client.put(
        "/api/sites/binary",
        content=b"\x80\x81\x82\xff",
        headers=auth_headers,
    )
    assert resp.status_code == 400
    assert "UTF-8" in resp.json()["detail"]


def test_put_file_too_large(client, auth_headers):
    from unittest.mock import patch

    # Patch to a small limit for the test
    with patch("sitedrop.server.routes_api.MAX_UPLOAD_BYTES", 100):
        resp = client.put(
            "/api/sites/huge",
            content=b"x" * 200,
            headers=auth_headers,
        )
    assert resp.status_code == 413
    assert "too large" in resp.json()["detail"].lower()


def test_put_exactly_at_limit(client, auth_headers):
    from unittest.mock import patch

    with patch("sitedrop.server.routes_api.MAX_UPLOAD_BYTES", 100):
        resp = client.put(
            "/api/sites/exact",
            content=b"x" * 100,
            headers=auth_headers,
        )
    assert resp.status_code == 200


def test_put_name_too_long(client, auth_headers):
    long_name = "a" * 241
    resp = client.put(
        f"/api/sites/{long_name}",
        content=b"<p>hi</p>",
        headers=auth_headers,
    )
    assert resp.status_code == 400
    assert "too long" in resp.json()["detail"].lower()


def test_put_name_at_limit(client, auth_headers):
    name = "a" * 240
    resp = client.put(
        f"/api/sites/{name}",
        content=b"<p>hi</p>",
        headers=auth_headers,
    )
    assert resp.status_code == 200
    assert resp.json()["name"] == name


def test_security_headers(client):
    resp = client.get("/nonexistent")
    assert resp.headers["X-Content-Type-Options"] == "nosniff"
    assert resp.headers["X-Frame-Options"] == "DENY"


def test_security_headers_on_api(client, auth_headers):
    resp = client.get("/api/sites", headers=auth_headers)
    assert resp.headers["X-Content-Type-Options"] == "nosniff"
    assert resp.headers["X-Frame-Options"] == "DENY"


def test_rate_limit_on_auth(client):
    from sitedrop.server.routes_api import AUTH_RATE_LIMIT

    for _ in range(AUTH_RATE_LIMIT):
        resp = client.post("/api/auth", json={"password": "wrong"})
        assert resp.status_code == 401

    # Next attempt should be rate limited
    resp = client.post("/api/auth", json={"password": "wrong"})
    assert resp.status_code == 429
    assert "Too many" in resp.json()["detail"]


def test_health_check(client):
    resp = client.get("/api/health")
    assert resp.status_code == 200
    assert resp.json() == {"status": "ok"}


def test_health_check_no_auth(client):
    resp = client.get("/api/health")
    assert resp.status_code == 200


def test_csp_on_api_response(client, auth_headers):
    resp = client.get("/api/sites", headers=auth_headers)
    assert (
        resp.headers["Content-Security-Policy"]
        == "default-src 'none'; frame-ancestors 'none'"
    )


def test_csp_on_health(client):
    resp = client.get("/api/health")
    assert (
        resp.headers["Content-Security-Policy"]
        == "default-src 'none'; frame-ancestors 'none'"
    )


def test_rate_limit_allows_after_window(client):
    import time

    from sitedrop.server.routes_api import AUTH_RATE_LIMIT, _auth_attempts

    # Fill up attempts
    for _ in range(AUTH_RATE_LIMIT):
        client.post("/api/auth", json={"password": "wrong"})

    # Simulate time passing by backdating all entries
    for ip in _auth_attempts:
        _auth_attempts[ip] = [time.time() - 120 for _ in _auth_attempts[ip]]

    # Should work again
    resp = client.post("/api/auth", json={"password": "testpass"})
    assert resp.status_code == 200


def test_sighup_handler_registered():
    import signal
    from unittest.mock import patch

    with patch("signal.signal") as mock_signal:
        from sitedrop.server.app import _register_sighup_handler

        _register_sighup_handler()
    mock_signal.assert_called_with(signal.SIGHUP, mock_signal.call_args[0][1])
