"""Tests for MatchNotifier — SendGrid mocked, no real emails."""

from __future__ import annotations

import logging
from datetime import datetime
from unittest.mock import MagicMock, call, patch

import pytest

from agent_samochodowy.config import Settings
from agent_samochodowy.dgred.client import DgredClient
from agent_samochodowy.dgred.notifier import MatchNotifier
from agent_samochodowy.dgred.writer import LeadWriter
from agent_samochodowy.models import MatchResult, Offer, PartQuery, Post


# ---------------------------------------------------------------------------
# Fixtures / helpers
# ---------------------------------------------------------------------------

def _settings(**overrides) -> Settings:
    defaults = dict(
        dgred_api_key="test",
        dry_run=True,
        sendgrid_api_key="SG.fake_key_for_tests",
        notify_email_from="sys@airminal.com",
        notify_email_to="wlodek@tender.pl,d@dgx.dev",
        notify_max_per_cycle=10,
    )
    defaults.update(overrides)
    return Settings(**defaults)


def _post(pid: str = "1", author: str = "Jan Kowalski") -> Post:
    return Post(
        post_id=pid,
        group="TIR części",
        author_name=author,
        author_profile_url=f"https://facebook.com/{author.replace(' ', '')}",
        text="Szukam pompy wody do Volvo FH13, numer 20744939",
        timestamp=datetime(2025, 1, 15, 10, 30),
        post_url=f"https://facebook.com/groups/tir/posts/{pid}",
    )


def _post_no_url(pid: str = "nourl") -> Post:
    return Post(
        post_id=pid,
        group="TIR części",
        author_name="Jan Kowalski",
        author_profile_url="https://facebook.com/JanKowalski",
        text="Szukam pompy wody do Volvo FH13",
        timestamp=datetime(2025, 1, 15, 10, 30),
        post_url="",
    )


def _query() -> PartQuery:
    return PartQuery(
        is_part_request=True,
        raw_text="Szukam pompy wody do Volvo FH13, numer 20744939",
        brand="VOLVO",
        model="FH13",
        category="pompa wody",
        oe_numbers=["20744939"],
        reason="Zapytanie o pompę wody z numerem OE",
    )


def _offer() -> Offer:
    return Offer(
        offer_id="1",
        platform="allegro",
        url="https://allegro.pl/1",
        title="Pompa wody VOLVO FH13 20744939",
        price=450.0,
        oe_numbers=["20744939"],
    )


def _matched_result() -> MatchResult:
    return MatchResult(
        confidence=0.95, method="oe", offers=[_offer()], matched=True,
    )


def _mock_sg_response(status_code=202):
    resp = MagicMock()
    resp.status_code = status_code
    resp.body = b""
    return resp


# ---------------------------------------------------------------------------
# MatchNotifier unit tests
# ---------------------------------------------------------------------------

class TestNotifierEnabled:
    def test_enabled_with_key_and_to(self):
        n = MatchNotifier(_settings())
        assert n.enabled is True

    def test_disabled_without_key(self):
        n = MatchNotifier(_settings(sendgrid_api_key=""))
        assert n.enabled is False

    def test_disabled_without_to(self):
        n = MatchNotifier(_settings(notify_email_to=""))
        assert n.enabled is False


class TestSeparateMailsPerRecipient:
    @patch("agent_samochodowy.dgred.notifier.MatchNotifier._send_single")
    def test_sends_separate_mail_per_recipient(self, mock_send):
        n = MatchNotifier(_settings())
        n.notify_match(_post(), _query(), _matched_result(), "c1", "reply")
        assert mock_send.call_count == 2
        recipients = [c.args[0] for c in mock_send.call_args_list]
        assert "wlodek@tender.pl" in recipients
        assert "d@dgx.dev" in recipients

    @patch("agent_samochodowy.dgred.notifier.MatchNotifier._send_single")
    def test_each_recipient_gets_same_subject_and_body(self, mock_send):
        n = MatchNotifier(_settings())
        n.notify_match(_post(), _query(), _matched_result(), "c1", "reply")
        subjects = [c.args[1] for c in mock_send.call_args_list]
        bodies = [c.args[2] for c in mock_send.call_args_list]
        assert subjects[0] == subjects[1]
        assert bodies[0] == bodies[1]

    @patch("agent_samochodowy.dgred.notifier.MatchNotifier._send_single")
    def test_single_recipient(self, mock_send):
        n = MatchNotifier(_settings(notify_email_to="solo@test.com"))
        n.notify_match(_post(), _query(), _matched_result(), "c1", "reply")
        assert mock_send.call_count == 1
        assert mock_send.call_args.args[0] == "solo@test.com"

    @patch("agent_samochodowy.dgred.notifier.MatchNotifier._send_single")
    def test_three_recipients(self, mock_send):
        n = MatchNotifier(_settings(notify_email_to="a@x.com,b@x.com,c@x.com"))
        n.notify_match(_post(), _query(), _matched_result(), "c1", "reply")
        assert mock_send.call_count == 3


class TestNotifierDedup:
    @patch("agent_samochodowy.dgred.notifier.MatchNotifier._send_single")
    def test_same_post_not_notified_twice(self, mock_send):
        n = MatchNotifier(_settings())
        post = _post("dup1")
        n.notify_match(post, _query(), _matched_result(), "c1", "reply")
        n.notify_match(post, _query(), _matched_result(), "c1", "reply")
        # 2 recipients × 1 notification = 2 calls (not 4)
        assert mock_send.call_count == 2

    @patch("agent_samochodowy.dgred.notifier.MatchNotifier._send_single")
    def test_different_posts_both_notified(self, mock_send):
        n = MatchNotifier(_settings())
        n.notify_match(_post("a"), _query(), _matched_result(), "c1", "reply")
        n.notify_match(_post("b"), _query(), _matched_result(), "c2", "reply")
        # 2 recipients × 2 posts = 4 calls
        assert mock_send.call_count == 4


class TestNotifierThrottle:
    @patch("agent_samochodowy.dgred.notifier.MatchNotifier._send_single")
    def test_throttle_counts_per_post_not_per_recipient(self, mock_send):
        """max=3 posts → 3 posts × 2 recipients = 6 _send_single calls."""
        n = MatchNotifier(_settings(notify_max_per_cycle=3))
        results = []
        for i in range(5):
            ok = n.notify_match(
                _post(str(i)), _query(), _matched_result(), f"c{i}", "reply",
            )
            results.append(ok)
        assert results == [True, True, True, False, False]
        # 3 posts × 2 recipients = 6 send calls
        assert mock_send.call_count == 6


class TestNotifierErrorHandling:
    @patch("agent_samochodowy.dgred.notifier.MatchNotifier._send_single",
           side_effect=RuntimeError("SendGrid down"))
    def test_error_logged_not_raised(self, mock_send, caplog):
        n = MatchNotifier(_settings())
        with caplog.at_level(logging.ERROR):
            ok = n.notify_match(_post(), _query(), _matched_result(), "c1", "reply")
        assert ok is False
        assert any("SendGrid error" in r.message for r in caplog.records)

    @patch("agent_samochodowy.dgred.notifier.MatchNotifier._send_single")
    def test_partial_failure_still_sends_to_others(self, mock_send):
        """If first recipient fails, second still gets mail."""
        mock_send.side_effect = [RuntimeError("fail"), None]
        n = MatchNotifier(_settings())
        ok = n.notify_match(_post(), _query(), _matched_result(), "c1", "reply")
        assert ok is False  # not all succeeded
        assert mock_send.call_count == 2  # both attempted

    def test_disabled_returns_false(self):
        n = MatchNotifier(_settings(sendgrid_api_key=""))
        ok = n.notify_match(_post(), _query(), _matched_result(), "c1", "reply")
        assert ok is False


class TestHtmlContent:
    @patch("agent_samochodowy.dgred.notifier.MatchNotifier._send_single")
    def test_subject_contains_part_and_vehicle(self, mock_send):
        n = MatchNotifier(_settings())
        n.notify_match(_post(), _query(), _matched_result(), "c1", "reply")
        subject = mock_send.call_args_list[0].args[1]
        assert "pompa wody" in subject
        assert "VOLVO" in subject

    @patch("agent_samochodowy.dgred.notifier.MatchNotifier._send_single")
    def test_html_contains_post_text(self, mock_send):
        n = MatchNotifier(_settings())
        n.notify_match(_post(), _query(), _matched_result(), "c1", "reply text here")
        html = mock_send.call_args_list[0].args[2]
        assert "Szukam pompy wody" in html
        assert "reply text here" in html

    @patch("agent_samochodowy.dgred.notifier.MatchNotifier._send_single")
    def test_html_contains_offer_details(self, mock_send):
        n = MatchNotifier(_settings())
        n.notify_match(_post(), _query(), _matched_result(), "c1", "reply")
        html = mock_send.call_args_list[0].args[2]
        assert "Pompa wody VOLVO FH13 20744939" in html
        assert "450 PLN" in html
        assert "https://allegro.pl/1" in html

    @patch("agent_samochodowy.dgred.notifier.MatchNotifier._send_single")
    def test_html_contains_dgred_link(self, mock_send):
        n = MatchNotifier(_settings())
        n.notify_match(_post(), _query(), _matched_result(), "c123", "reply")
        html = mock_send.call_args_list[0].args[2]
        assert "/client/c123" in html
        assert "Otwórz lead w dgred" in html

    @patch("agent_samochodowy.dgred.notifier.MatchNotifier._send_single")
    def test_html_contains_author_profile_link(self, mock_send):
        n = MatchNotifier(_settings())
        n.notify_match(_post(), _query(), _matched_result(), "c1", "reply")
        html = mock_send.call_args_list[0].args[2]
        assert "https://facebook.com/JanKowalski" in html
        assert "Profil autora" in html

    @patch("agent_samochodowy.dgred.notifier.MatchNotifier._send_single")
    def test_html_contains_post_url_button(self, mock_send):
        """Post URL → 'Otwórz post na Facebooku' button."""
        n = MatchNotifier(_settings())
        n.notify_match(_post(), _query(), _matched_result(), "c1", "reply")
        html = mock_send.call_args_list[0].args[2]
        assert "Otwórz post na Facebooku" in html
        assert "facebook.com/groups/tir/posts/1" in html

    @patch("agent_samochodowy.dgred.notifier.MatchNotifier._send_single")
    def test_html_missing_post_url_shows_warning(self, mock_send):
        """No post_url → warning text instead of button."""
        n = MatchNotifier(_settings())
        n.notify_match(_post_no_url(), _query(), _matched_result(), "c1", "reply")
        html = mock_send.call_args_list[0].args[2]
        assert "Brak linku do posta" in html
        assert "Otwórz post na Facebooku" not in html

    @patch("agent_samochodowy.dgred.notifier.MatchNotifier._send_single")
    def test_html_has_proper_structure(self, mock_send):
        """Verify the key HTML structural elements."""
        n = MatchNotifier(_settings())
        n.notify_match(_post(), _query(), _matched_result(), "c1", "gotowa odpowiedź")
        html = mock_send.call_args_list[0].args[2]
        # Header section
        assert "Nowe dopasowanie" in html
        # Zapytanie section
        assert "Zapytanie z grupy" in html
        assert "TIR cz" in html  # escaped group name
        # Offer card
        assert "Dopasowana oferta" in html
        assert "Zobacz ofertę" in html
        # Reply proposal section
        assert "Gotowa propozycja odpowiedzi" in html
        assert "gotowa odpowiedź" in html
        # Footer
        assert "Agent Samochodowy" in html

    @patch("agent_samochodowy.dgred.notifier.MatchNotifier._send_single")
    def test_html_escapes_special_chars(self, mock_send):
        """HTML special chars in post text must be escaped."""
        post = Post(
            post_id="esc1", group="Test <Group>", author_name="A & B",
            author_profile_url="https://fb.com/ab",
            text='Szukam części "specjalnej" <TIR> & inne',
            timestamp=datetime(2025, 1, 15), post_url="https://fb.com/p/1",
        )
        n = MatchNotifier(_settings())
        n.notify_match(post, _query(), _matched_result(), "c1", "reply <tag>")
        html = mock_send.call_args_list[0].args[2]
        assert "&lt;TIR&gt;" in html
        assert "&amp; inne" in html
        assert "A &amp; B" in html
        assert "&lt;tag&gt;" in html

    @patch("agent_samochodowy.dgred.notifier.MatchNotifier._send_single")
    def test_html_no_offer(self, mock_send):
        """No offers → no offer card, but rest of mail still works."""
        result = MatchResult(confidence=0.95, method="oe", offers=[], matched=True)
        n = MatchNotifier(_settings())
        n.notify_match(_post(), _query(), result, "c1", "reply")
        html = mock_send.call_args_list[0].args[2]
        assert "Dopasowana oferta" not in html
        assert "Gotowa propozycja odpowiedzi" in html


class TestSendGridIntegration:
    @patch("sendgrid.SendGridAPIClient")
    def test_send_single_calls_sendgrid_api(self, MockSGClient):
        mock_instance = MagicMock()
        mock_instance.send.return_value = _mock_sg_response(202)
        MockSGClient.return_value = mock_instance

        n = MatchNotifier(_settings())
        n._send_single("test@example.com", "Subject", "<p>Body</p>")

        MockSGClient.assert_called_once_with("SG.fake_key_for_tests")
        mock_instance.send.assert_called_once()

    @patch("sendgrid.SendGridAPIClient")
    def test_send_single_raises_on_4xx(self, MockSGClient):
        mock_instance = MagicMock()
        mock_instance.send.return_value = _mock_sg_response(400)
        MockSGClient.return_value = mock_instance

        n = MatchNotifier(_settings())
        with pytest.raises(RuntimeError, match="SendGrid HTTP 400"):
            n._send_single("test@example.com", "Subject", "<p>Body</p>")

    @patch("sendgrid.SendGridAPIClient")
    def test_full_flow_sends_separate_api_calls(self, MockSGClient):
        """Each recipient triggers a separate SendGrid API call."""
        mock_instance = MagicMock()
        mock_instance.send.return_value = _mock_sg_response(202)
        MockSGClient.return_value = mock_instance

        n = MatchNotifier(_settings())
        n.notify_match(_post(), _query(), _matched_result(), "c1", "reply")

        # 2 recipients → 2 SendGridAPIClient instantiations → 2 send() calls
        assert mock_instance.send.call_count == 2


# ---------------------------------------------------------------------------
# Writer integration — notifier called only for status 67
# ---------------------------------------------------------------------------

class TestWriterNotifierIntegration:
    @patch("agent_samochodowy.dgred.notifier.MatchNotifier._send_single")
    def test_notifier_called_for_dopasowano(self, mock_send):
        settings = _settings()
        dgred = DgredClient(settings)
        writer = LeadWriter(dgred, settings)
        writer.write_lead(_post(), _query(), _matched_result())
        # 2 recipients
        assert mock_send.call_count == 2
        dgred.close()

    @patch("agent_samochodowy.dgred.notifier.MatchNotifier._send_single")
    def test_notifier_not_called_for_review(self, mock_send):
        settings = _settings()
        dgred = DgredClient(settings)
        writer = LeadWriter(dgred, settings)
        result = MatchResult(confidence=0.60, method="atrybuty",
                             offers=[_offer()], matched=True)
        writer.write_lead(_post(), _query(), result)
        mock_send.assert_not_called()
        dgred.close()

    @patch("agent_samochodowy.dgred.notifier.MatchNotifier._send_single")
    def test_notifier_not_called_for_no_match(self, mock_send):
        settings = _settings()
        dgred = DgredClient(settings)
        writer = LeadWriter(dgred, settings)
        result = MatchResult(confidence=0.0, method="brak", matched=False)
        writer.write_lead(_post(), _query(), result)
        mock_send.assert_not_called()
        dgred.close()

    @patch("agent_samochodowy.dgred.notifier.MatchNotifier._send_single",
           side_effect=RuntimeError("boom"))
    def test_sendgrid_failure_does_not_break_pipeline(self, mock_send, caplog):
        """SendGrid error must NOT propagate — lead writing must succeed."""
        settings = _settings()
        dgred = DgredClient(settings)
        writer = LeadWriter(dgred, settings)
        with caplog.at_level(logging.INFO):
            writer.write_lead(_post(), _query(), _matched_result())
        assert any("Lead written" in r.message for r in caplog.records)
        dgred.close()
