import json
import sqlite3
from urllib import error
from urllib.parse import parse_qs, urlparse

import pytest
from fastapi.testclient import TestClient

from app.db import reset_db_caches
from app.config import Settings, get_settings
from app.integrations.ticktick import (
    AUTH_SCOPE,
    TICKTICK_AUTH_URL,
    TickTickClient,
    TickTickTask,
    default_auth_state_store,
)
from app.main import create_app


class _FakeJsonResponse:
    def __init__(self, status_code: int, payload):
        self.status_code = status_code
        self.payload = payload

    def getcode(self) -> int:
        return self.status_code

    def read(self) -> bytes:
        return json.dumps(self.payload).encode("utf-8")

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc, tb) -> None:
        return None


def _configured_settings(**overrides) -> Settings:
    payload = {
        "app_env": "development",
        "app_hostname": "localhost:8000",
        "ticktick_client_id": "ticktick-client-id",
        "ticktick_client_secret": "ticktick-client-secret",
        "ticktick_token": "ticktick-access-token",
        "home_assistant_action_task_project_id": "project-123",
    }
    payload.update(overrides)
    return Settings(_env_file=None, **payload)


def test_build_authorization_url_contains_expected_query(monkeypatch: pytest.MonkeyPatch) -> None:
    client = TickTickClient(settings=_configured_settings())
    monkeypatch.setattr("app.integrations.ticktick.secrets.token_hex", lambda _: "state-123")

    authorization_url = client.build_authorization_url()
    parsed = urlparse(authorization_url)
    query = parse_qs(parsed.query)

    assert f"{parsed.scheme}://{parsed.netloc}{parsed.path}" == TICKTICK_AUTH_URL
    assert query["client_id"] == ["ticktick-client-id"]
    assert query["response_type"] == ["code"]
    assert query["redirect_uri"] == ["http://localhost:8000/ticktick/auth/code"]
    assert query["state"] == ["state-123"]
    assert query["scope"] == [AUTH_SCOPE]


def test_exchange_authorization_code_posts_expected_request(monkeypatch: pytest.MonkeyPatch) -> None:
    captured = {}
    client = TickTickClient(settings=_configured_settings())
    default_auth_state_store.pending_state = "expected-state"

    def fake_urlopen(req, timeout):
        captured["url"] = req.full_url
        captured["timeout"] = timeout
        captured["authorization"] = req.headers["Authorization"]
        captured["content_type"] = req.headers["Content-type"]
        captured["body"] = req.data.decode("utf-8")
        return _FakeJsonResponse(200, {"access_token": "new-token"})

    monkeypatch.setattr("app.integrations.ticktick.request.urlopen", fake_urlopen)

    token = client.exchange_authorization_code(code="oauth-code", state="expected-state")

    assert token == "new-token"
    assert captured["url"] == "https://ticktick.com/oauth/token"
    assert captured["timeout"] == pytest.approx(10.0)
    assert captured["content_type"] == "application/x-www-form-urlencoded"
    assert captured["authorization"].startswith("Basic ")
    assert "code=oauth-code" in captured["body"]
    assert "grant_type=authorization_code" in captured["body"]
    assert "scope=tasks%3Aread+tasks%3Awrite" in captured["body"]
    assert "client_id=" not in captured["body"]
    assert "client_secret=" not in captured["body"]


def test_exchange_authorization_code_trims_ticktick_config_values(monkeypatch: pytest.MonkeyPatch) -> None:
    captured = {}
    client = TickTickClient(
        settings=_configured_settings(
            app_hostname=" localhost:8000 ",
            ticktick_client_id=" ticktick-client-id ",
            ticktick_client_secret=" ticktick-client-secret ",
        )
    )
    default_auth_state_store.pending_state = "trimmed-state"

    def fake_urlopen(req, timeout):
        captured["authorization"] = req.headers["Authorization"]
        captured["body"] = req.data.decode("utf-8")
        return _FakeJsonResponse(200, {"access_token": "trimmed-token"})

    monkeypatch.setattr("app.integrations.ticktick.request.urlopen", fake_urlopen)

    token = client.exchange_authorization_code(code="oauth-code", state="trimmed-state")

    assert token == "trimmed-token"
    assert captured["authorization"].startswith("Basic ")
    assert "redirect_uri=http%3A%2F%2Flocalhost%3A8000%2Fticktick%2Fauth%2Fcode" in captured["body"]


def test_create_task_skips_duplicate_titles(monkeypatch: pytest.MonkeyPatch) -> None:
    client = TickTickClient(settings=_configured_settings())

    def fake_urlopen(req, timeout):
        assert req.full_url.endswith("/project/project-123/data")
        return _FakeJsonResponse(
            200,
            {
                "tasks": [
                    {
                        "id": "task-1",
                        "projectId": "project-123",
                        "title": "wash dishes",
                        "columnId": "column-7",
                    }
                ]
            },
        )

    monkeypatch.setattr("app.integrations.ticktick.request.urlopen", fake_urlopen)

    client.create_task(TickTickTask(projectId="project-123", title="wash dishes"))


def test_get_projects_ignores_unknown_fields(monkeypatch: pytest.MonkeyPatch) -> None:
    client = TickTickClient(settings=_configured_settings())

    def fake_urlopen(req, timeout):
        assert req.full_url.endswith("/project/")
        return _FakeJsonResponse(
            200,
            [
                {
                    "id": "project-123",
                    "name": "Inbox",
                    "etag": "project-etag",
                }
            ],
        )

    monkeypatch.setattr("app.integrations.ticktick.request.urlopen", fake_urlopen)

    projects = client.get_projects()

    assert len(projects) == 1
    assert projects[0].id == "project-123"
    assert projects[0].name == "Inbox"


def test_create_task_posts_expected_payload(monkeypatch: pytest.MonkeyPatch) -> None:
    captured = {"calls": []}
    client = TickTickClient(settings=_configured_settings())

    def fake_urlopen(req, timeout):
        captured["calls"].append(req.full_url)
        if req.full_url.endswith("/project/project-123/data"):
            return _FakeJsonResponse(200, {"tasks": []})
        captured["authorization"] = req.headers["Authorization"]
        captured["content_type"] = req.headers["Content-type"]
        captured["body"] = json.loads(req.data.decode("utf-8"))
        return _FakeJsonResponse(200, {"id": "task-99"})

    monkeypatch.setattr("app.integrations.ticktick.request.urlopen", fake_urlopen)

    client.create_task(
        TickTickTask(projectId="project-123", title="wash dishes", dueDate="2026-04-21T00:00:00+0000")
    )

    assert captured["calls"] == [
        "https://api.ticktick.com/open/v1/project/project-123/data",
        "https://api.ticktick.com/open/v1/task",
    ]
    assert captured["authorization"] == "Bearer ticktick-access-token"
    assert captured["content_type"] == "application/json"
    assert captured["body"] == {
        "projectId": "project-123",
        "title": "wash dishes",
        "dueDate": "2026-04-21T00:00:00+0000",
    }


def test_homeassistant_publish_creates_ticktick_action_task(
    auth_database,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    monkeypatch.setenv("APP_HOSTNAME", "localhost:8000")
    monkeypatch.setenv("TICKTICK_CLIENT_ID", "ticktick-client-id")
    monkeypatch.setenv("TICKTICK_CLIENT_SECRET", "ticktick-client-secret")
    monkeypatch.setenv("TICKTICK_TOKEN", "ticktick-access-token")
    monkeypatch.setenv("HOME_ASSISTANT_ACTION_TASK_PROJECT_ID", "project-123")
    get_settings.cache_clear()
    reset_db_caches()

    captured = {"calls": []}

    def fake_urlopen(req, timeout):
        captured["calls"].append(req.full_url)
        if req.full_url.endswith("/project/project-123/data"):
            return _FakeJsonResponse(200, {"tasks": []})
        captured["body"] = json.loads(req.data.decode("utf-8"))
        return _FakeJsonResponse(200, {"id": "task-1"})

    monkeypatch.setattr("app.integrations.ticktick.request.urlopen", fake_urlopen)

    with TestClient(create_app()) as client:
        response = client.post(
            "/homeassistant/publish",
            json={
                "target": "ticktick",
                "action": "create_action_task",
                "content": "{'title': 'ignored', 'action': 'take out trash', 'due_hour': 6}",
            },
        )

    assert response.status_code == 200
    assert captured["calls"] == [
        "https://api.ticktick.com/open/v1/project/project-123/data",
        "https://api.ticktick.com/open/v1/task",
    ]
    assert captured["body"]["projectId"] == "project-123"
    assert captured["body"]["title"] == "take out trash"
    assert captured["body"]["dueDate"].endswith("+0000")


def test_ticktick_auth_start_redirects_authenticated_user(
    auth_database,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    monkeypatch.setenv("APP_HOSTNAME", "localhost:8000")
    monkeypatch.setenv("TICKTICK_CLIENT_ID", "ticktick-client-id")
    monkeypatch.setenv("TICKTICK_CLIENT_SECRET", "ticktick-client-secret")
    get_settings.cache_clear()
    reset_db_caches()
    monkeypatch.setattr("app.integrations.ticktick.secrets.token_hex", lambda _: "state-redirect")

    with TestClient(create_app()) as client:
        resp = client.post(
            "/api/auth/login",
            json={"username": "admin", "password": "test-password"},
        )
        assert resp.status_code == 200, f"API login failed: {resp.status_code}"

        response = client.get("/ticktick/auth/start", follow_redirects=False)

    assert response.status_code == 303
    parsed = urlparse(response.headers["location"])
    query = parse_qs(parsed.query)
    assert f"{parsed.scheme}://{parsed.netloc}{parsed.path}" == TICKTICK_AUTH_URL
    assert query["state"] == ["state-redirect"]


def test_ticktick_auth_callback_persists_token(
    auth_database,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    monkeypatch.setenv("APP_HOSTNAME", "localhost:8000")
    monkeypatch.setenv("TICKTICK_CLIENT_ID", "ticktick-client-id")
    monkeypatch.setenv("TICKTICK_CLIENT_SECRET", "ticktick-client-secret")
    get_settings.cache_clear()
    reset_db_caches()
    default_auth_state_store.pending_state = "callback-state"

    def fake_urlopen(req, timeout):
        return _FakeJsonResponse(200, {"access_token": "persisted-token"})

    monkeypatch.setattr("app.integrations.ticktick.request.urlopen", fake_urlopen)

    with TestClient(create_app()) as client:
        response = client.get(
            "/ticktick/auth/code?state=callback-state&code=oauth-code",
            follow_redirects=False,
        )

    assert response.status_code == 303
    assert response.headers["location"] == "/config?ticktick_oauth=success"

    conn = sqlite3.connect(auth_database["app_path"])
    try:
        row = conn.execute(
            "SELECT value FROM app_config WHERE key = ?",
            ("TICKTICK_TOKEN",),
        ).fetchone()
    finally:
        conn.close()

    assert row is not None
    assert row[0] == "persisted-token"


def test_ticktick_auth_callback_redirects_on_invalid_state(
    auth_database,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    monkeypatch.setenv("APP_HOSTNAME", "localhost:8000")
    monkeypatch.setenv("TICKTICK_CLIENT_ID", "ticktick-client-id")
    monkeypatch.setenv("TICKTICK_CLIENT_SECRET", "ticktick-client-secret")
    get_settings.cache_clear()
    reset_db_caches()
    default_auth_state_store.pending_state = "expected-state"

    with TestClient(create_app()) as client:
        response = client.get(
            "/ticktick/auth/code?state=wrong-state&code=oauth-code",
            follow_redirects=False,
        )

    assert response.status_code == 303
    assert response.headers["location"] == "/config?ticktick_oauth=invalid-state"


def test_ticktick_auth_callback_redirects_when_token_exchange_fails(
    auth_database,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    monkeypatch.setenv("APP_HOSTNAME", "localhost:8000")
    monkeypatch.setenv("TICKTICK_CLIENT_ID", "ticktick-client-id")
    monkeypatch.setenv("TICKTICK_CLIENT_SECRET", "ticktick-client-secret")
    get_settings.cache_clear()
    reset_db_caches()
    default_auth_state_store.pending_state = "callback-state"

    def fake_urlopen(req, timeout):
        raise error.HTTPError(req.full_url, 401, "Unauthorized", hdrs=None, fp=None)

    monkeypatch.setattr("app.integrations.ticktick.request.urlopen", fake_urlopen)

    with TestClient(create_app()) as client:
        response = client.get(
            "/ticktick/auth/code?state=callback-state&code=oauth-code",
            follow_redirects=False,
        )

    assert response.status_code == 303
    assert response.headers["location"] == "/config?ticktick_oauth=failed"