"""Deep coverage tests for research_routes.py targeting uncovered branches.

Targeted functions and branches NOT covered by existing tests:
- terminate_research: not found, already terminal, not active sets suspended,
  progress_log as JSON string, socket emit failure, db exception
- delete_research: not found, in-progress active rejection, success path,
  db exception
- clear_history: with active IDs, without active IDs, db exception
- get_history: success with title, db exception
- get_research_details: not found, success, db exception
- get_research_logs: not found, db exception
- get_research_report: research not found, content None, db exception
- export_research_report: unsupported format, research not found,
  report content not found
- get_research_status: not found, error metadata timeout classification,
  milestone log present, db exception
- get_queue_status: success, exception
- get_queue_position: not in queue, success, exception
- open_file_location: always 403
- get_upload_limits: returns config
- upload_pdf: no files key, empty filename
- start_research: non-JSON body, missing query, missing model,
  OPENAI_ENDPOINT without custom_endpoint
- redirect_static: redirects
"""

import io
import uuid
from datetime import datetime
from contextlib import contextmanager
from unittest.mock import MagicMock, patch

import pytest
from flask import Flask

MODULE = "local_deep_research.web.routes.research_routes"


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _uid():
    return uuid.uuid4().hex[:8]


def _make_research(
    id="res-1",
    query="test query",
    mode="quick",
    status="in_progress",
    created_at="2025-01-01T00:00:00+00:00",
    completed_at=None,
    progress=50,
    report_path=None,
    research_meta=None,
    progress_log=None,
    title=None,
):
    """Create a mock ResearchHistory object."""
    r = MagicMock()
    r.id = id
    r.query = query
    r.mode = mode
    r.status = status
    r.created_at = created_at
    r.completed_at = completed_at
    r.progress = progress
    r.report_path = report_path
    r.research_meta = research_meta if research_meta is not None else {}
    r.progress_log = progress_log
    r.title = title
    r.chat_session_id = None
    return r


def _make_milestone(message="Phase complete", level="MILESTONE"):
    """Create a mock ResearchLog milestone row."""
    entry = MagicMock()
    entry.id = 1
    entry.message = message
    entry.timestamp = datetime(2025, 1, 1, 12, 0, 0)
    entry.level = level
    return entry


def _mock_db_session():
    """Create a MagicMock that works as a SQLAlchemy session."""
    return MagicMock()


@contextmanager
def _ctx(session):
    """Context manager wrapping a mock session."""
    yield session


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


@pytest.fixture()
def app():
    """Create a minimal Flask app with the research blueprint, auth bypassed."""
    flask_app = Flask(__name__)
    flask_app.secret_key = "test-secret-key"
    flask_app.config["TESTING"] = True

    from local_deep_research.web.routes.research_routes import research_bp

    flask_app.register_blueprint(research_bp)

    # Bypass login_required by making db_manager think user is connected
    with patch("local_deep_research.web.auth.decorators.db_manager") as mock_db:
        mock_db.is_user_connected.return_value = True
        yield flask_app


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


@pytest.fixture(autouse=True)
def _inject_session(app):
    """Inject authenticated session for every request."""

    @app.before_request
    def _set_sess():
        from flask import session

        session["username"] = "testuser"
        session["session_id"] = "sid-1"


# ---------------------------------------------------------------------------
# terminate_research
# ---------------------------------------------------------------------------


class TestTerminateResearch:
    def test_terminate_not_found(self, client):
        """Returns 404 when research ID does not exist."""
        ms = _mock_db_session()
        ms.query.return_value.filter_by.return_value.first.return_value = None
        with patch(f"{MODULE}.get_user_db_session", return_value=_ctx(ms)):
            resp = client.post("/api/terminate/no-such-id")
        assert resp.status_code == 404

    def test_terminate_already_completed(self, client):
        """Returns success when research is in a terminal state."""
        research = _make_research(status="completed")
        ms = _mock_db_session()
        ms.query.return_value.filter_by.return_value.first.return_value = (
            research
        )
        with patch(f"{MODULE}.get_user_db_session", return_value=_ctx(ms)):
            resp = client.post("/api/terminate/res-1")
        assert resp.status_code == 200
        assert resp.get_json()["status"] == "success"

    def test_terminate_not_active_sets_suspended(self, client):
        """Sets status to suspended when research is not tracked in globals."""
        research = _make_research(status="in_progress")
        ms = _mock_db_session()
        ms.query.return_value.filter_by.return_value.first.return_value = (
            research
        )
        with (
            patch(f"{MODULE}.get_user_db_session", return_value=_ctx(ms)),
            patch(f"{MODULE}.is_research_active", return_value=False),
        ):
            resp = client.post("/api/terminate/res-1")
        assert resp.status_code == 200
        assert research.status == "suspended"

    def test_terminate_active_string_progress_log(self, client):
        """Handles progress_log stored as a JSON string."""
        research = _make_research(
            status="in_progress",
            progress_log='[{"time":"t","progress":0}]',
        )
        ms = _mock_db_session()
        ms.query.return_value.filter_by.return_value.first.return_value = (
            research
        )
        with (
            patch(f"{MODULE}.get_user_db_session", return_value=_ctx(ms)),
            patch(f"{MODULE}.is_research_active", return_value=True),
            patch(f"{MODULE}.set_termination_flag"),
            patch(f"{MODULE}.get_research_field", return_value=50),
            patch(f"{MODULE}.append_research_log"),
            patch(f"{MODULE}.logger"),
            patch(
                "local_deep_research.web.services.socket_service.SocketIOService"
            ) as mock_sio,
        ):
            mock_sio.return_value.emit_socket_event = MagicMock()
            resp = client.post("/api/terminate/res-1")
        assert resp.status_code == 200
        # progress_log should now be a list with appended entry
        assert isinstance(research.progress_log, list)
        assert len(research.progress_log) == 2

    def test_terminate_db_exception(self, client):
        """Returns 500 on database error."""
        with patch(
            f"{MODULE}.get_user_db_session", side_effect=Exception("db")
        ):
            resp = client.post("/api/terminate/res-1")
        assert resp.status_code == 500


# ---------------------------------------------------------------------------
# delete_research
# ---------------------------------------------------------------------------


class TestDeleteResearch:
    def test_delete_not_found(self, client):
        """Returns 404 when research not found."""
        ms = _mock_db_session()
        ms.query.return_value.filter_by.return_value.first.return_value = None
        with patch(f"{MODULE}.get_user_db_session", return_value=_ctx(ms)):
            resp = client.delete("/api/delete/res-1")
        assert resp.status_code == 404

    def test_delete_in_progress_active(self, client):
        """Returns 400 when research is actively running."""
        research = _make_research(status="in_progress")
        ms = _mock_db_session()
        ms.query.return_value.filter_by.return_value.first.return_value = (
            research
        )
        with (
            patch(f"{MODULE}.get_user_db_session", return_value=_ctx(ms)),
            patch(f"{MODULE}.is_research_active", return_value=True),
        ):
            resp = client.delete("/api/delete/res-1")
        assert resp.status_code == 400

    def test_delete_success(self, client):
        """Successfully deletes completed research."""
        research = _make_research(status="completed", report_path=None)
        ms = _mock_db_session()
        ms.query.return_value.filter_by.return_value.first.return_value = (
            research
        )
        with patch(f"{MODULE}.get_user_db_session", return_value=_ctx(ms)):
            resp = client.delete("/api/delete/res-1")
        assert resp.status_code == 200

    def test_delete_db_exception(self, client):
        """Returns 500 on database error."""
        with patch(
            f"{MODULE}.get_user_db_session", side_effect=Exception("db")
        ):
            resp = client.delete("/api/delete/res-1")
        assert resp.status_code == 500


# ---------------------------------------------------------------------------
# clear_history
# ---------------------------------------------------------------------------


class TestClearHistory:
    def test_clear_with_active_ids(self, client):
        """Skips active research when clearing."""
        research = _make_research(
            id="old", status="completed", report_path=None
        )
        ms = _mock_db_session()
        ms.query.return_value.all.return_value = [research]
        with (
            patch(f"{MODULE}.get_user_db_session", return_value=_ctx(ms)),
            patch(
                f"{MODULE}.get_active_research_ids", return_value=["active-1"]
            ),
        ):
            resp = client.post("/api/clear_history")
        assert resp.status_code == 200

    def test_clear_no_active_ids(self, client):
        """Deletes all when no active research."""
        ms = _mock_db_session()
        ms.query.return_value.all.return_value = []
        with (
            patch(f"{MODULE}.get_user_db_session", return_value=_ctx(ms)),
            patch(f"{MODULE}.get_active_research_ids", return_value=[]),
        ):
            resp = client.post("/api/clear_history")
        assert resp.status_code == 200

    def test_clear_db_exception(self, client):
        """Returns 500 on database error."""
        with patch(
            f"{MODULE}.get_user_db_session", side_effect=Exception("db")
        ):
            resp = client.post("/api/clear_history")
        assert resp.status_code == 500


# ---------------------------------------------------------------------------
# get_history
# ---------------------------------------------------------------------------


class TestGetHistory:
    def test_history_success_with_title(self, client):
        """Returns history items including title."""
        research = _make_research(
            status="completed",
            completed_at="2025-01-01T01:00:00+00:00",
            title="My Research",
        )
        ms = _mock_db_session()
        ms.query.return_value.order_by.return_value.limit.return_value.offset.return_value.all.return_value = [
            research
        ]
        ms.query.return_value.filter_by.return_value.count.return_value = 3
        with (
            patch(f"{MODULE}.get_user_db_session", return_value=_ctx(ms)),
            patch(f"{MODULE}.calculate_duration", return_value=3600),
            patch(f"{MODULE}.filter_research_metadata", return_value={}),
        ):
            resp = client.get("/api/history")
        assert resp.status_code == 200
        data = resp.get_json()
        assert data["status"] == "success"
        assert len(data["items"]) == 1
        assert data["items"][0]["title"] == "My Research"

    def test_history_db_exception(self, client):
        """Returns 500 on database error."""
        with patch(
            f"{MODULE}.get_user_db_session", side_effect=Exception("db")
        ):
            resp = client.get("/api/history")
        assert resp.status_code == 500


# ---------------------------------------------------------------------------
# get_research_details
# ---------------------------------------------------------------------------


class TestGetResearchDetails:
    def test_details_not_found(self, client):
        """Returns 404 when research not found."""
        ms = _mock_db_session()
        ms.query.return_value.filter.return_value.first.return_value = None
        with patch(f"{MODULE}.get_user_db_session", return_value=_ctx(ms)):
            resp = client.get("/api/research/no-id")
        assert resp.status_code == 404

    def test_details_success(self, client):
        """Returns research details."""
        research = _make_research()
        ms = _mock_db_session()
        ms.query.return_value.filter.return_value.first.return_value = research
        with (
            patch(f"{MODULE}.get_user_db_session", return_value=_ctx(ms)),
            patch(f"{MODULE}.strip_settings_snapshot", return_value={}),
        ):
            resp = client.get("/api/research/res-1")
        assert resp.status_code == 200
        assert resp.get_json()["id"] == "res-1"

    def test_details_db_exception(self, client):
        """Returns 500 on database error."""
        with patch(
            f"{MODULE}.get_user_db_session", side_effect=Exception("db")
        ):
            resp = client.get("/api/research/res-1")
        assert resp.status_code == 500


# ---------------------------------------------------------------------------
# get_research_logs
# ---------------------------------------------------------------------------


class TestGetResearchLogs:
    def test_logs_not_found(self, client):
        """Returns 404 when research not found."""
        ms = _mock_db_session()
        ms.query.return_value.filter_by.return_value.first.return_value = None
        with patch(f"{MODULE}.get_user_db_session", return_value=_ctx(ms)):
            resp = client.get("/api/research/no-id/logs")
        assert resp.status_code == 404

    def test_logs_db_exception(self, client):
        """Returns 500 on database error."""
        with patch(
            f"{MODULE}.get_user_db_session", side_effect=Exception("db")
        ):
            resp = client.get("/api/research/res-1/logs")
        assert resp.status_code == 500


# ---------------------------------------------------------------------------
# get_research_report
# ---------------------------------------------------------------------------


# ---------------------------------------------------------------------------
# get_research_status
# ---------------------------------------------------------------------------


class TestGetResearchStatus:
    def test_status_not_found(self, client):
        """Returns 404 when research not found."""
        ms = _mock_db_session()
        ms.query.return_value.filter_by.return_value.first.return_value = None
        with patch(f"{MODULE}.get_user_db_session", return_value=_ctx(ms)):
            resp = client.get("/api/research/res-1/status")
        assert resp.status_code == 404

    def test_status_with_timeout_error(self, client):
        """Includes timeout error_info in metadata."""
        research = _make_research(
            status="failed",
            research_meta={"error": "Request Timeout after 120s"},
        )
        ms = _mock_db_session()
        ms.query.return_value.filter_by.return_value.first.return_value = (
            research
        )
        # Second query call for milestone - returns None
        ms.query.return_value.filter_by.return_value.order_by.return_value.first.return_value = None
        with (
            patch(f"{MODULE}.get_user_db_session", return_value=_ctx(ms)),
            patch(
                f"{MODULE}.strip_settings_snapshot",
                return_value={"error": "Request Timeout after 120s"},
            ),
        ):
            resp = client.get("/api/research/res-1/status")
        assert resp.status_code == 200
        data = resp.get_json()
        assert data["metadata"]["error_info"]["type"] == "timeout"

    def test_status_with_milestone_log(self, client):
        """Includes log_entry when milestone found."""
        research = _make_research(status="in_progress", research_meta={})
        milestone = _make_milestone(message="Phase 2 done")
        ms = _mock_db_session()
        ms.query.return_value.filter_by.return_value.first.return_value = (
            research
        )
        ms.query.return_value.filter_by.return_value.order_by.return_value.first.return_value = milestone
        with (
            patch(f"{MODULE}.get_user_db_session", return_value=_ctx(ms)),
            patch(f"{MODULE}.strip_settings_snapshot", return_value={}),
        ):
            resp = client.get("/api/research/res-1/status")
        assert resp.status_code == 200
        data = resp.get_json()
        assert data["log_entry"]["message"] == "Phase 2 done"

    def test_status_db_exception(self, client):
        """Returns 500 on database error."""
        with patch(
            f"{MODULE}.get_user_db_session", side_effect=Exception("db")
        ):
            resp = client.get("/api/research/res-1/status")
        assert resp.status_code == 500


# ---------------------------------------------------------------------------
# get_queue_status / get_queue_position
# ---------------------------------------------------------------------------


# ---------------------------------------------------------------------------
# open_file_location
# ---------------------------------------------------------------------------


class TestOpenFileLocation:
    def test_always_returns_403(self, client):
        """Feature disabled in server mode."""
        resp = client.post("/open_file_location")
        assert resp.status_code == 403
        assert "disabled" in resp.get_json()["message"].lower()


# ---------------------------------------------------------------------------
# get_upload_limits
# ---------------------------------------------------------------------------


class TestGetUploadLimits:
    def test_returns_limits(self, client):
        """Returns upload config limits."""
        resp = client.get("/api/config/limits")
        assert resp.status_code == 200
        data = resp.get_json()
        assert "max_file_size" in data
        assert "max_files" in data
        assert "allowed_mime_types" in data


# ---------------------------------------------------------------------------
# upload_pdf
# ---------------------------------------------------------------------------


class TestUploadPdf:
    def test_no_files_key(self, client):
        """Returns 400 when files key missing."""
        resp = client.post(
            "/api/upload/pdf", data={}, content_type="multipart/form-data"
        )
        assert resp.status_code in (400, 500)

    def test_empty_filename(self, client):
        """Returns 400 when file has empty filename."""
        data = {"files": (io.BytesIO(b""), "")}
        resp = client.post(
            "/api/upload/pdf",
            data=data,
            content_type="multipart/form-data",
        )
        assert resp.status_code in (400, 500)


# ---------------------------------------------------------------------------
# redirect_static
# ---------------------------------------------------------------------------


class TestRedirectStatic:
    def test_redirects(self, client):
        """Redirects old static URLs."""
        resp = client.get("/redirect-static/css/style.css")
        assert resp.status_code in (302, 308)


# ---------------------------------------------------------------------------
# start_research (validation)
# ---------------------------------------------------------------------------
