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

import pytest

from datatypes.magicplan.api.response import MagicPlanPlan, PlanSummary
from datatypes.magicplan.domain.mapper import map_plan
from datatypes.magicplan.domain.models import Plan

from backend.app.db.models.uploaded_file import (
    FileSourceEnum,
    FileTypeEnum,
    UploadedFile,
)
from backend.magic_plan.magic_plan_client import MagicPlanClient
from backend.magic_plan.magic_plan_service import MagicPlanService
from backend.magic_plan.magic_plan_trigger_request import MagicPlanTriggerRequest

FIXTURE_DIR = Path(__file__).parents[2] / "magic_plan"
PLAN_ID = "a7285ed1-878d-47eb-8aa6-85ef9e187516"
S3_BUCKET = "test-bucket"


@pytest.fixture(scope="module")
def domain_plan() -> Plan:
    data = json.loads(
        (FIXTURE_DIR / "magicplan_api_plan_response_example.json").read_text()
    )
    return map_plan(MagicPlanPlan.model_validate(data["data"]))


@pytest.fixture(scope="module")
def api_magic_plan() -> MagicPlanPlan:
    data = json.loads(
        (FIXTURE_DIR / "magicplan_api_plan_response_example.json").read_text()
    )
    return MagicPlanPlan.model_validate(data["data"])


@pytest.fixture(scope="module")
def plan_summary() -> PlanSummary:
    data = json.loads(
        (FIXTURE_DIR / "magicplan_api_plan_response_example.json").read_text()
    )
    return MagicPlanPlan.model_validate(data["data"]).plan


@pytest.fixture()
def mock_client() -> MagicMock:
    client = MagicMock(spec=MagicPlanClient)
    client.get_plan_raw.return_value = (
        FIXTURE_DIR / "magicplan_api_plan_response_example.json"
    ).read_bytes()
    return client


def _make_service(mock_client: MagicMock) -> MagicPlanService:
    return MagicPlanService(client=mock_client, s3_bucket=S3_BUCKET)


def _make_request(
    address: str = "2 Laburnum Way Bromley BR2 8BZ",
    hubspot_deal_id: str = "deal-123",
    uprn: str | None = None,
) -> MagicPlanTriggerRequest:
    return MagicPlanTriggerRequest(
        address=address, hubspot_deal_id=hubspot_deal_id, uprn=uprn
    )


# --- no match ---


def test_run_raises_when_no_plan_found(mock_client: MagicMock) -> None:
    # Arrange
    mock_client.get_plans.return_value.plans = []
    service = _make_service(mock_client)
    # Act / Assert
    with pytest.raises(ValueError, match="No MagicPlan found"):
        service.run(_make_request(address="99 Nowhere Road London SW1A 1AA"))


# --- match found ---


def test_run_fetches_plan_with_matched_id(
    mock_client: MagicMock,
    api_magic_plan: MagicPlanPlan,
    plan_summary: PlanSummary,
    domain_plan: Plan,
) -> None:
    # Arrange
    mock_client.get_plans.return_value = [plan_summary]
    mock_client.get_plan.return_value = api_magic_plan
    service = _make_service(mock_client)
    with patch(
        "backend.magic_plan.magic_plan_service.find_matching_plan",
        return_value=plan_summary,
    ), patch("backend.magic_plan.magic_plan_service.save_plan"), patch(
        "backend.magic_plan.magic_plan_service.db_session"
    ), patch(
        "backend.magic_plan.magic_plan_service.save_data_to_s3"
    ):
        service.run(_make_request())
    # Assert
    mock_client.get_plan_raw.assert_called_once_with(plan_summary.id)


def test_run_returns_mapped_plan(
    mock_client: MagicMock,
    api_magic_plan: MagicPlanPlan,
    plan_summary: PlanSummary,
    domain_plan: Plan,
) -> None:
    # Arrange
    mock_client.get_plans.return_value = [plan_summary]
    mock_client.get_plan.return_value = api_magic_plan
    service = _make_service(mock_client)
    with patch(
        "backend.magic_plan.magic_plan_service.find_matching_plan",
        return_value=plan_summary,
    ), patch("backend.magic_plan.magic_plan_service.save_plan"), patch(
        "backend.magic_plan.magic_plan_service.db_session"
    ), patch(
        "backend.magic_plan.magic_plan_service.save_data_to_s3"
    ):
        result = service.run(_make_request())
    # Assert
    assert isinstance(result, Plan)
    assert result.uid == PLAN_ID


def test_run_calls_save_plan_with_mapped_plan(
    mock_client: MagicMock,
    api_magic_plan: MagicPlanPlan,
    plan_summary: PlanSummary,
) -> None:
    # Arrange
    mock_client.get_plans.return_value = [plan_summary]
    mock_client.get_plan.return_value = api_magic_plan
    service = _make_service(mock_client)
    with patch(
        "backend.magic_plan.magic_plan_service.find_matching_plan",
        return_value=plan_summary,
    ), patch("backend.magic_plan.magic_plan_service.save_plan") as mock_save, patch(
        "backend.magic_plan.magic_plan_service.db_session"
    ), patch(
        "backend.magic_plan.magic_plan_service.save_data_to_s3"
    ):
        service.run(_make_request())
    # Assert — save_plan called with a Plan whose uid matches
    call_args = mock_save.call_args
    saved_plan: Plan = call_args[0][1]
    assert saved_plan.uid == PLAN_ID


def test_run_accepts_uprn_without_error(
    mock_client: MagicMock,
    api_magic_plan: MagicPlanPlan,
    plan_summary: PlanSummary,
) -> None:
    # Arrange
    mock_client.get_plans.return_value = [plan_summary]
    mock_client.get_plan.return_value = api_magic_plan
    service = _make_service(mock_client)
    with patch(
        "backend.magic_plan.magic_plan_service.find_matching_plan",
        return_value=plan_summary,
    ), patch("backend.magic_plan.magic_plan_service.save_plan"), patch(
        "backend.magic_plan.magic_plan_service.db_session"
    ), patch(
        "backend.magic_plan.magic_plan_service.save_data_to_s3"
    ):
        service.run(_make_request(uprn="100023336956"))


# --- S3 upload ---


def test_run_uploads_to_s3_with_uprn_key(
    mock_client: MagicMock,
    api_magic_plan: MagicPlanPlan,
    plan_summary: PlanSummary,
) -> None:
    # Arrange
    mock_client.get_plans.return_value = [plan_summary]
    request = _make_request(uprn="100023336956")
    service = MagicPlanService(client=mock_client, s3_bucket=S3_BUCKET)
    with patch(
        "backend.magic_plan.magic_plan_service.find_matching_plan",
        return_value=plan_summary,
    ), patch("backend.magic_plan.magic_plan_service.save_plan"), patch(
        "backend.magic_plan.magic_plan_service.db_session"
    ), patch(
        "backend.magic_plan.magic_plan_service.save_data_to_s3"
    ) as mock_s3:
        # Act
        service.run(request)
    # Assert
    mock_s3.assert_called_once_with(
        ANY,
        S3_BUCKET,
        f"documents/uprn/100023336956/magic_plan_{plan_summary.id}.json.gz",
    )


def test_run_uploads_to_s3_with_deal_id_key_when_uprn_absent(
    mock_client: MagicMock,
    api_magic_plan: MagicPlanPlan,
    plan_summary: PlanSummary,
) -> None:
    # Arrange
    mock_client.get_plans.return_value = [plan_summary]
    mock_client.get_plan.return_value = api_magic_plan
    request = _make_request(hubspot_deal_id="deal-456", uprn=None)
    service = MagicPlanService(client=mock_client, s3_bucket=S3_BUCKET)
    with patch(
        "backend.magic_plan.magic_plan_service.find_matching_plan",
        return_value=plan_summary,
    ), patch("backend.magic_plan.magic_plan_service.save_plan"), patch(
        "backend.magic_plan.magic_plan_service.db_session"
    ), patch(
        "backend.magic_plan.magic_plan_service.save_data_to_s3"
    ) as mock_s3:
        # Act
        service.run(request)
    # Assert
    mock_s3.assert_called_once_with(
        ANY,
        S3_BUCKET,
        f"documents/hubspot_deal_id/deal-456/magic_plan_{plan_summary.id}.json.gz",
    )


# --- UploadedFile record ---


def test_run_creates_uploaded_file_record(
    mock_client: MagicMock,
    api_magic_plan: MagicPlanPlan,
    plan_summary: PlanSummary,
) -> None:
    # Arrange
    mock_client.get_plans.return_value = [plan_summary]
    mock_client.get_plan.return_value = api_magic_plan
    request = _make_request(hubspot_deal_id="deal-789", uprn="100023336956")
    service = MagicPlanService(client=mock_client, s3_bucket=S3_BUCKET)
    mock_session = MagicMock()
    with patch(
        "backend.magic_plan.magic_plan_service.find_matching_plan",
        return_value=plan_summary,
    ), patch("backend.magic_plan.magic_plan_service.save_plan"), patch(
        "backend.magic_plan.magic_plan_service.db_session"
    ) as mock_db, patch(
        "backend.magic_plan.magic_plan_service.save_data_to_s3"
    ):
        mock_db.return_value.__enter__.return_value = mock_session
        # Act
        service.run(request)
    # Assert
    added_objects = [call.args[0] for call in mock_session.add.call_args_list]
    uploaded_file = next(
        (obj for obj in added_objects if isinstance(obj, UploadedFile)), None
    )
    assert uploaded_file is not None
    assert uploaded_file.file_source == FileSourceEnum.MAGIC_PLAN.value
    assert uploaded_file.file_type == FileTypeEnum.MAGIC_PLAN_JSON.value
    assert uploaded_file.s3_file_bucket == S3_BUCKET
    assert uploaded_file.s3_file_key == f"documents/uprn/100023336956/magic_plan_{plan_summary.id}.json.gz"
    assert uploaded_file.s3_upload_timestamp is not None
    assert uploaded_file.uprn == 100023336956
    assert uploaded_file.hubspot_deal_id == "deal-789"
