"""Tests for context overflow API logic paths not covered by existing tests.

Focus: pagination clamping, empty/null states, chart data token formula.
Production code: src/local_deep_research/web/routes/context_overflow_api.py

Uses Flask test client approach from test_context_overflow_api_http.py.
"""

from contextlib import contextmanager
from datetime import datetime, timezone
from unittest.mock import Mock, patch

from flask import Flask, jsonify

from local_deep_research.web.auth.routes import auth_bp
from local_deep_research.web.routes.context_overflow_api import (
    context_overflow_bp,
)

_ROUTES_MOD = "local_deep_research.web.routes.context_overflow_api"


# ---------------------------------------------------------------------------
# Test Infrastructure
# ---------------------------------------------------------------------------


def _create_test_app():
    app = Flask(__name__)
    app.config["SECRET_KEY"] = "test-secret"
    app.config["WTF_CSRF_ENABLED"] = False
    app.register_blueprint(auth_bp)
    app.register_blueprint(context_overflow_bp)

    @app.errorhandler(500)
    def _handle_500(error):
        return jsonify({"error": "Internal server error"}), 500

    return app


@contextmanager
def _authenticated_client(app):
    """Provide test client with mocked auth and DB session."""
    mock_db = Mock()
    mock_db.connections = {"testuser": True}
    mock_db.has_encryption = False

    _mock_query = Mock()
    _mock_query.all.return_value = []
    _mock_query.first.return_value = None
    _mock_query.count.return_value = 0
    _mock_query.scalar.return_value = 0
    _mock_query.filter_by.return_value = _mock_query
    _mock_query.filter.return_value = _mock_query
    _mock_query.order_by.return_value = _mock_query
    _mock_query.limit.return_value = _mock_query
    _mock_query.offset.return_value = _mock_query
    _mock_query.group_by.return_value = _mock_query
    _mock_query.with_entities.return_value = _mock_query

    _mock_db_session = Mock()
    _mock_db_session.query.return_value = _mock_query

    @contextmanager
    def _fake_session(*args, **kwargs):
        yield _mock_db_session

    patches = [
        patch("local_deep_research.web.auth.decorators.db_manager", mock_db),
        patch(f"{_ROUTES_MOD}.get_user_db_session", side_effect=_fake_session),
        patch(
            f"{_ROUTES_MOD}.SettingsManager",
            return_value=Mock(get_setting=Mock(return_value=8192)),
        ),
    ]

    started = []
    try:
        for p in patches:
            started.append(p.start())
        with app.test_client() as client:
            with client.session_transaction() as sess:
                sess["username"] = "testuser"
                sess["session_id"] = "test-session-id"
            yield client, _mock_db_session, _mock_query
    finally:
        for p in patches:
            p.stop()


def _make_overview_and_token_rows(
    total_requests=0,
    requests_with_context=0,
    truncated_requests=0,
):
    """Create the merged overview row (counts + AVG + token-summary fields).

    The route now issues a single .first() call. The legacy two-row return
    shape is kept so existing call sites don't change — `tk` is the same
    object as `ov`, since fields are merged onto a single mock.
    """
    overview_row = Mock(
        total_requests=total_requests,
        requests_with_context=requests_with_context,
        truncated_requests=truncated_requests,
        avg_tokens_truncated=0,
        total_tokens=0,
        total_prompt_tokens=0,
        total_completion_tokens=0,
        avg_prompt_tokens=0,
        avg_completion_tokens=0,
        max_prompt_tokens=0,
    )
    return overview_row, overview_row


def _setup_multi_first(mock_query, overview_row, token_row):
    """Wire mock_query.first() to return the (merged) overview row."""
    mock_query.first.return_value = overview_row
    mock_query.scalar.return_value = 0


# ---------------------------------------------------------------------------
# Pagination logic
# ---------------------------------------------------------------------------


class TestPaginationLogic:
    """Tests for pagination parameter clamping."""

    def test_per_page_clamped_to_500(self):
        app = _create_test_app()
        with _authenticated_client(app) as (client, _, mock_query):
            ov, tk = _make_overview_and_token_rows()
            _setup_multi_first(mock_query, ov, tk)

            resp = client.get("/api/context-overflow?per_page=1000")
            assert resp.status_code == 200
            assert resp.get_json()["pagination"]["per_page"] == 500

    def test_per_page_zero_clamped_to_1(self):
        app = _create_test_app()
        with _authenticated_client(app) as (client, _, mock_query):
            ov, tk = _make_overview_and_token_rows()
            _setup_multi_first(mock_query, ov, tk)

            resp = client.get("/api/context-overflow?per_page=0")
            assert resp.status_code == 200
            assert resp.get_json()["pagination"]["per_page"] == 1

    def test_per_page_negative_clamped_to_1(self):
        app = _create_test_app()
        with _authenticated_client(app) as (client, _, mock_query):
            ov, tk = _make_overview_and_token_rows()
            _setup_multi_first(mock_query, ov, tk)

            resp = client.get("/api/context-overflow?per_page=-5")
            assert resp.status_code == 200
            assert resp.get_json()["pagination"]["per_page"] == 1

    def test_page_zero_clamped_to_1(self):
        app = _create_test_app()
        with _authenticated_client(app) as (client, _, mock_query):
            ov, tk = _make_overview_and_token_rows()
            _setup_multi_first(mock_query, ov, tk)

            resp = client.get("/api/context-overflow?page=0")
            assert resp.status_code == 200
            assert resp.get_json()["pagination"]["page"] == 1

    def test_page_2_with_items(self):
        """page=2, per_page=10 with 15 total → correct pagination metadata."""
        app = _create_test_app()
        with _authenticated_client(app) as (client, _, mock_query):
            ov, tk = _make_overview_and_token_rows(total_requests=15)
            _setup_multi_first(mock_query, ov, tk)
            mock_query.count.return_value = 15

            resp = client.get("/api/context-overflow?page=2&per_page=10")
            assert resp.status_code == 200
            data = resp.get_json()
            assert data["pagination"]["page"] == 2
            assert data["pagination"]["per_page"] == 10
            assert data["pagination"]["total_count"] == 15
            assert data["pagination"]["total_pages"] == 2


# ---------------------------------------------------------------------------
# Empty/null state
# ---------------------------------------------------------------------------


class TestEmptyNullStates:
    """Tests for zero/null edge cases."""

    def test_zero_records_overview_all_zeros(self):
        app = _create_test_app()
        with _authenticated_client(app) as (client, _, mock_query):
            ov, tk = _make_overview_and_token_rows()
            _setup_multi_first(mock_query, ov, tk)

            resp = client.get("/api/context-overflow")
            assert resp.status_code == 200
            data = resp.get_json()
            assert data["overview"]["total_requests"] == 0
            assert data["overview"]["truncation_rate"] == 0
            assert data["chart_data"] == []

    def test_requests_with_context_zero_truncation_rate_zero(self):
        """requests_with_context=0 → truncation_rate=0 (not NaN)."""
        app = _create_test_app()
        with _authenticated_client(app) as (client, _, mock_query):
            ov, tk = _make_overview_and_token_rows(
                total_requests=5, requests_with_context=0, truncated_requests=0
            )
            _setup_multi_first(mock_query, ov, tk)

            resp = client.get("/api/context-overflow")
            assert resp.status_code == 200
            assert resp.get_json()["overview"]["truncation_rate"] == 0

    def test_avg_tokens_truncated_none_becomes_zero(self):
        """scalar() returns None → avg_tokens_truncated=0."""
        app = _create_test_app()
        with _authenticated_client(app) as (client, _, mock_query):
            ov, tk = _make_overview_and_token_rows()
            _setup_multi_first(mock_query, ov, tk)
            # scalar for avg_tokens_truncated returns None
            mock_query.scalar.return_value = None

            resp = client.get("/api/context-overflow")
            assert resp.status_code == 200
            assert resp.get_json()["overview"]["avg_tokens_truncated"] == 0

    def test_nonexistent_research_id_empty_response(self):
        app = _create_test_app()
        with _authenticated_client(app) as (client, _, mock_query):
            mock_query.all.return_value = []

            resp = client.get("/api/research/nonexistent-id/context-overflow")
            assert resp.status_code == 200
            data = resp.get_json()
            assert data["status"] == "success"
            assert data["data"]["overview"]["total_requests"] == 0

    def test_all_context_truncated_false(self):
        """All records have context_truncated=False → truncated_requests=0."""
        app = _create_test_app()
        with _authenticated_client(app) as (client, _, mock_query):
            ov, tk = _make_overview_and_token_rows(
                total_requests=3, requests_with_context=3, truncated_requests=0
            )
            _setup_multi_first(mock_query, ov, tk)

            resp = client.get("/api/context-overflow")
            assert resp.status_code == 200
            data = resp.get_json()
            assert data["overview"]["truncated_requests"] == 0
            assert data["overview"]["truncation_rate"] == 0


# ---------------------------------------------------------------------------
# Chart data token formula
# ---------------------------------------------------------------------------


def _make_usage_mock(
    prompt_tokens=100,
    completion_tokens=20,
    ollama_prompt_eval_count=None,
    context_truncated=False,
    tokens_truncated=0,
    context_limit=4096,
    research_id="res-1",
    model_name="gpt-4",
    model_provider="openai",
    research_query="test",
    research_phase="search",
    total_tokens=120,
    truncation_ratio=None,
    ollama_eval_count=None,
    calling_function="search",
    response_time_ms=500,
):
    """Create a mock TokenUsage record."""
    m = Mock()
    m.prompt_tokens = prompt_tokens
    m.completion_tokens = completion_tokens
    m.ollama_prompt_eval_count = ollama_prompt_eval_count
    m.context_truncated = context_truncated
    m.tokens_truncated = tokens_truncated
    m.context_limit = context_limit
    m.research_id = research_id
    m.model_name = model_name
    m.model_provider = model_provider
    m.research_query = research_query
    m.research_phase = research_phase
    m.total_tokens = total_tokens
    m.truncation_ratio = truncation_ratio
    m.calling_function = calling_function
    m.response_time_ms = response_time_ms
    m.timestamp = datetime(2024, 1, 1, tzinfo=timezone.utc)
    return m


def _setup_chart_data_mocks(mock_query, ov, tk, usage_list):
    """Set up mocks for chart_data tests.

    The production code calls .all() 7 times in order:
    1. model_token_query 2. phase_query 3. context_limits
    4. recent_truncated 5. time_series_data 6. model_stats 7. all_requests_data
    We put usage_list at position 5 (time_series) and 7 (all_requests), empty elsewhere.
    """
    _setup_multi_first(mock_query, ov, tk)
    mock_query.all.side_effect = [
        [],  # model_token_query
        [],  # phase_query
        [],  # context_limits
        [],  # recent_truncated
        usage_list,  # time_series_data → chart_data
        [],  # model_stats
        usage_list,  # all_requests_data
    ]
    mock_query.count.return_value = len(usage_list)


class TestChartDataTokenFormula:
    """Tests for chart_data token calculation logic."""

    def test_ollama_prompt_eval_count_used_when_present(self):
        """ollama_prompt_eval_count present → used instead of prompt_tokens."""
        app = _create_test_app()
        with _authenticated_client(app) as (client, _, mock_query):
            usage = _make_usage_mock(
                prompt_tokens=100,
                ollama_prompt_eval_count=80,
                context_truncated=False,
            )
            ov, tk = _make_overview_and_token_rows(
                total_requests=1, requests_with_context=1
            )
            _setup_chart_data_mocks(mock_query, ov, tk, [usage])

            resp = client.get("/api/context-overflow")
            assert resp.status_code == 200
            chart = resp.get_json()["chart_data"]
            assert len(chart) == 1
            assert chart[0]["ollama_prompt_tokens"] == 80
            # original = 80 (ollama used), no truncation
            assert chart[0]["original_prompt_tokens"] == 80

    def test_ollama_none_falls_back_to_prompt_tokens(self):
        """ollama_prompt_eval_count=None → falls back to prompt_tokens."""
        app = _create_test_app()
        with _authenticated_client(app) as (client, _, mock_query):
            usage = _make_usage_mock(
                prompt_tokens=100,
                ollama_prompt_eval_count=None,
                context_truncated=False,
            )
            ov, tk = _make_overview_and_token_rows(total_requests=1)
            _setup_chart_data_mocks(mock_query, ov, tk, [usage])

            resp = client.get("/api/context-overflow")
            assert resp.status_code == 200
            chart = resp.get_json()["chart_data"]
            assert chart[0]["original_prompt_tokens"] == 100

    def test_truncated_adds_tokens_truncated(self):
        """context_truncated=True → original = prompt + tokens_truncated."""
        app = _create_test_app()
        with _authenticated_client(app) as (client, _, mock_query):
            usage = _make_usage_mock(
                prompt_tokens=100,
                ollama_prompt_eval_count=None,
                context_truncated=True,
                tokens_truncated=500,
            )
            ov, tk = _make_overview_and_token_rows(
                total_requests=1, requests_with_context=1, truncated_requests=1
            )
            _setup_chart_data_mocks(mock_query, ov, tk, [usage])

            resp = client.get("/api/context-overflow")
            assert resp.status_code == 200
            chart = resp.get_json()["chart_data"]
            assert chart[0]["original_prompt_tokens"] == 600  # 100 + 500

    def test_not_truncated_no_addition(self):
        """context_truncated=False → original = prompt (no addition)."""
        app = _create_test_app()
        with _authenticated_client(app) as (client, _, mock_query):
            usage = _make_usage_mock(
                prompt_tokens=200,
                context_truncated=False,
                tokens_truncated=0,
            )
            ov, tk = _make_overview_and_token_rows(total_requests=1)
            _setup_chart_data_mocks(mock_query, ov, tk, [usage])

            resp = client.get("/api/context-overflow")
            assert resp.status_code == 200
            chart = resp.get_json()["chart_data"]
            assert chart[0]["original_prompt_tokens"] == 200

    def test_all_null_token_fields_no_crash(self):
        """All null token fields → no crash, 0 values."""
        app = _create_test_app()
        with _authenticated_client(app) as (client, _, mock_query):
            usage = _make_usage_mock(
                prompt_tokens=0,
                completion_tokens=0,
                ollama_prompt_eval_count=None,
                context_truncated=False,
                tokens_truncated=None,
                total_tokens=0,
            )
            ov, tk = _make_overview_and_token_rows(total_requests=1)
            _setup_chart_data_mocks(mock_query, ov, tk, [usage])

            resp = client.get("/api/context-overflow")
            assert resp.status_code == 200
            chart = resp.get_json()["chart_data"]
            assert chart[0]["original_prompt_tokens"] == 0
            assert chart[0]["tokens_truncated"] == 0
