"""Unit tests for the Fitbit source adapter."""

import json
from pathlib import Path
from unittest.mock import patch, MagicMock

import pytest

from byteb4rb1e.homeostat.envelope import Envelope
from byteb4rb1e.homeostat.adapters.fitbit import (
    FitbitAdapter,
    RateLimited,
    TokenStore,
    _generate_pkce,
)


# -- Fixtures ----------------------------------------------------------------


class FakeHttpResponse:
    def __init__(self, status_code, data, headers=None):
        self.status_code = status_code
        self.data = json.dumps(data).encode() if isinstance(
            data, dict
        ) else data
        self.headers = headers or {}
        self.text = self.data.decode("utf-8", errors="replace")

    def json(self):
        return json.loads(self.data)


@pytest.fixture
def token_store(tmp_path):
    path = tmp_path / ".fitbit_tokens.json"
    tokens = {
        "access_token": "test_access",
        "refresh_token": "test_refresh",
    }
    path.write_text(json.dumps(tokens))
    return TokenStore(path=path)


@pytest.fixture
def adapter(token_store):
    return FitbitAdapter(
        client_id="test_id",
        client_secret="test_secret",
        token_store=token_store,
    )


# -- TokenStore --------------------------------------------------------------


class TestTokenStore:
    def test_load_returns_none_when_missing(self, tmp_path):
        store = TokenStore(path=tmp_path / "missing.json")
        assert store.load() is None

    def test_load_returns_tokens(self, tmp_path):
        path = tmp_path / "tokens.json"
        tokens = {"access_token": "abc", "refresh_token": "xyz"}
        path.write_text(json.dumps(tokens))
        store = TokenStore(path=path)
        assert store.load() == tokens

    def test_save_persists_tokens(self, tmp_path):
        path = tmp_path / "tokens.json"
        store = TokenStore(path=path)
        tokens = {"access_token": "new", "refresh_token": "new_r"}
        store.save(tokens)
        assert json.loads(path.read_text()) == tokens


# -- Envelope ----------------------------------------------------------------


class TestEnvelope:
    def test_envelope_is_frozen(self):
        env = Envelope(
            source="fitbit",
            endpoint="/test",
            fetched_at="2026-03-20T00:00:00+00:00",
            payload={"key": "value"},
        )
        with pytest.raises(AttributeError):
            env.source = "other"

    def test_envelope_fields(self):
        env = Envelope(
            source="fitbit",
            endpoint="/test",
            fetched_at="2026-03-20T00:00:00+00:00",
            payload={"steps": 8000},
        )
        assert env.source == "fitbit"
        assert env.endpoint == "/test"
        assert env.payload == {"steps": 8000}


# -- PKCE --------------------------------------------------------------------


class TestPKCE:
    def test_verifier_length(self):
        verifier, _ = _generate_pkce()
        assert len(verifier) <= 128

    def test_challenge_is_base64url(self):
        _, challenge = _generate_pkce()
        assert "+" not in challenge
        assert "/" not in challenge
        assert "=" not in challenge


# -- FitbitAdapter.get -------------------------------------------------------


class TestFitbitAdapterGet:
    @patch("byteb4rb1e.homeostat.adapters.fitbit.http_get")
    def test_successful_get_returns_envelope(
        self, mock_get, adapter
    ):
        mock_get.return_value = FakeHttpResponse(
            200, {"activities-steps": [{"value": "8000"}]}
        )
        env = adapter.get("/1/user/-/activities/steps/date/today/1d.json")

        assert isinstance(env, Envelope)
        assert env.source == "fitbit"
        assert env.endpoint == (
            "/1/user/-/activities/steps/date/today/1d.json"
        )
        assert env.payload == {
            "activities-steps": [{"value": "8000"}]
        }
        assert env.fetched_at  # non-empty ISO timestamp

    @patch("byteb4rb1e.homeostat.adapters.fitbit.http_post")
    @patch("byteb4rb1e.homeostat.adapters.fitbit.http_get")
    def test_401_triggers_refresh_and_retry(
        self, mock_get, mock_post, adapter
    ):
        expired_resp = FakeHttpResponse(401, {"error": "expired"})
        ok_resp = FakeHttpResponse(
            200, {"data": "refreshed"}
        )
        mock_get.side_effect = [expired_resp, ok_resp]

        mock_post.return_value = FakeHttpResponse(200, {
            "access_token": "new_access",
            "refresh_token": "new_refresh",
        })

        env = adapter.get("/test")
        assert env.payload == {"data": "refreshed"}
        assert mock_get.call_count == 2
        assert mock_post.call_count == 1

    @patch("byteb4rb1e.homeostat.adapters.fitbit.http_get")
    def test_429_raises_rate_limited(self, mock_get, adapter):
        mock_get.return_value = FakeHttpResponse(
            429, b"",
            headers={"Fitbit-Rate-Limit-Reset": "120"},
        )
        with pytest.raises(RateLimited) as exc_info:
            adapter.get("/test")
        assert exc_info.value.reset_seconds == 120

    @patch("byteb4rb1e.homeostat.adapters.fitbit.http_get")
    def test_other_error_raises_runtime_error(
        self, mock_get, adapter
    ):
        mock_get.return_value = FakeHttpResponse(
            500, b"Internal Server Error"
        )
        with pytest.raises(RuntimeError, match="500"):
            adapter.get("/test")

    @patch("byteb4rb1e.homeostat.adapters.fitbit.http_post")
    @patch("byteb4rb1e.homeostat.adapters.fitbit.http_get")
    def test_refresh_persists_new_tokens(
        self, mock_get, mock_post, adapter, token_store
    ):
        mock_get.side_effect = [
            FakeHttpResponse(401, {}),
            FakeHttpResponse(200, {"ok": True}),
        ]
        new_tokens = {
            "access_token": "refreshed_access",
            "refresh_token": "refreshed_refresh",
        }
        mock_post.return_value = FakeHttpResponse(
            200, new_tokens
        )

        adapter.get("/test")

        saved = token_store.load()
        assert saved["access_token"] == "refreshed_access"
        assert saved["refresh_token"] == "refreshed_refresh"


# -- Endpoint helpers --------------------------------------------------------


class TestEndpointHelpers:
    @patch("byteb4rb1e.homeostat.adapters.fitbit.http_get")
    def test_heart_rate_endpoint(self, mock_get, adapter):
        mock_get.return_value = FakeHttpResponse(200, {})
        adapter.heart_rate("2026-03-19")
        url = mock_get.call_args[0][0]
        assert "/activities/heart/date/2026-03-19/1d.json" in url

    @patch("byteb4rb1e.homeostat.adapters.fitbit.http_get")
    def test_heart_rate_intraday_endpoint(
        self, mock_get, adapter
    ):
        mock_get.return_value = FakeHttpResponse(200, {})
        adapter.heart_rate_intraday(
            "2026-03-19", "14:00", "14:15"
        )
        url = mock_get.call_args[0][0]
        assert "/1min/time/14:00/14:15.json" in url

    @patch("byteb4rb1e.homeostat.adapters.fitbit.http_get")
    def test_steps_endpoint(self, mock_get, adapter):
        mock_get.return_value = FakeHttpResponse(200, {})
        adapter.steps("2026-03-19")
        url = mock_get.call_args[0][0]
        assert "/activities/steps/date/2026-03-19/1d.json" in url

    @patch("byteb4rb1e.homeostat.adapters.fitbit.http_get")
    def test_sleep_endpoint(self, mock_get, adapter):
        mock_get.return_value = FakeHttpResponse(200, {})
        adapter.sleep("2026-03-19")
        url = mock_get.call_args[0][0]
        assert "/sleep/date/2026-03-19.json" in url

    @patch("byteb4rb1e.homeostat.adapters.fitbit.http_get")
    def test_body_endpoint(self, mock_get, adapter):
        mock_get.return_value = FakeHttpResponse(200, {})
        adapter.body("2026-03-19")
        url = mock_get.call_args[0][0]
        assert "/body/date/2026-03-19.json" in url

    @patch("byteb4rb1e.homeostat.adapters.fitbit.http_get")
    def test_activities_endpoint(self, mock_get, adapter):
        mock_get.return_value = FakeHttpResponse(200, {})
        adapter.activities("2026-03-19")
        url = mock_get.call_args[0][0]
        assert "/activities/date/2026-03-19.json" in url

    @patch("byteb4rb1e.homeostat.adapters.fitbit.http_get")
    def test_range_endpoints_include_both_dates(
        self, mock_get, adapter
    ):
        mock_get.return_value = FakeHttpResponse(200, {})
        adapter.steps_range("2026-03-01", "2026-03-19")
        url = mock_get.call_args[0][0]
        assert "2026-03-01" in url
        assert "2026-03-19" in url
