"""Tests for the background monitor: should_alert logic, store alert methods, and run_monitor_search."""
from __future__ import annotations

import sqlite3
from datetime import datetime, timedelta, timezone
from pathlib import Path
from unittest.mock import patch

import pytest

from app.tasks.monitor import _AUCTION_ALERT_WINDOW_HOURS, should_alert

# ---------------------------------------------------------------------------
# should_alert — pure function, no I/O
# ---------------------------------------------------------------------------


class TestShouldAlert:
    def test_bin_above_threshold_alerts(self):
        assert should_alert(
            trust_score=70, score_is_partial=False,
            price=100.0, buying_format="fixed_price",
            min_trust_score=60,
        ) is True

    def test_bin_below_threshold_no_alert(self):
        assert should_alert(
            trust_score=55, score_is_partial=False,
            price=100.0, buying_format="fixed_price",
            min_trust_score=60,
        ) is False

    def test_partial_score_applies_buffer(self):
        # Score 65 with min 60 passes normally but fails with the +10 partial buffer.
        assert should_alert(
            trust_score=65, score_is_partial=True,
            price=100.0, buying_format="fixed_price",
            min_trust_score=60,
        ) is False

    def test_partial_score_above_buffered_threshold_alerts(self):
        assert should_alert(
            trust_score=75, score_is_partial=True,
            price=100.0, buying_format="fixed_price",
            min_trust_score=60,
        ) is True

    def test_best_offer_treated_like_bin(self):
        assert should_alert(
            trust_score=80, score_is_partial=False,
            price=200.0, buying_format="best_offer",
            min_trust_score=60,
        ) is True

    def test_auction_within_window_alerts(self):
        soon = (datetime.now(timezone.utc) + timedelta(hours=12)).isoformat()
        assert should_alert(
            trust_score=70, score_is_partial=False,
            price=100.0, buying_format="auction",
            min_trust_score=60, ends_at=soon,
        ) is True

    def test_auction_outside_window_no_alert(self):
        far = (datetime.now(timezone.utc) + timedelta(hours=48)).isoformat()
        assert should_alert(
            trust_score=70, score_is_partial=False,
            price=100.0, buying_format="auction",
            min_trust_score=60, ends_at=far,
        ) is False

    def test_auction_no_ends_at_alerts_anyway(self):
        assert should_alert(
            trust_score=70, score_is_partial=False,
            price=100.0, buying_format="auction",
            min_trust_score=60, ends_at=None,
        ) is True

    def test_auction_bad_ends_at_alerts_anyway(self):
        assert should_alert(
            trust_score=70, score_is_partial=False,
            price=100.0, buying_format="auction",
            min_trust_score=60, ends_at="not-a-date",
        ) is True

    def test_auction_expired_no_alert(self):
        past = (datetime.now(timezone.utc) - timedelta(hours=1)).isoformat()
        assert should_alert(
            trust_score=70, score_is_partial=False,
            price=100.0, buying_format="auction",
            min_trust_score=60, ends_at=past,
        ) is False

    def test_unknown_format_alerts(self):
        # Fail-open: unknown buying_format should not silently suppress.
        assert should_alert(
            trust_score=70, score_is_partial=False,
            price=100.0, buying_format="mystery_format",
            min_trust_score=60,
        ) is True

    def test_score_exactly_at_threshold_passes(self):
        assert should_alert(
            trust_score=60, score_is_partial=False,
            price=100.0, buying_format="fixed_price",
            min_trust_score=60,
        ) is True

    def test_auction_exactly_at_window_boundary_alerts(self):
        boundary = (datetime.now(timezone.utc) + timedelta(hours=_AUCTION_ALERT_WINDOW_HOURS - 0.1)).isoformat()
        assert should_alert(
            trust_score=70, score_is_partial=False,
            price=100.0, buying_format="auction",
            min_trust_score=60, ends_at=boundary,
        ) is True


# ---------------------------------------------------------------------------
# Store alert methods — integration against real SQLite
# ---------------------------------------------------------------------------


def _create_monitor_db(path: Path) -> None:
    conn = sqlite3.connect(path)
    conn.executescript("""
        CREATE TABLE IF NOT EXISTS saved_searches (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            name TEXT NOT NULL,
            query TEXT NOT NULL,
            platform TEXT NOT NULL DEFAULT 'ebay',
            filters_json TEXT,
            created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
            last_run_at TEXT,
            monitor_enabled INTEGER NOT NULL DEFAULT 0,
            poll_interval_min INTEGER NOT NULL DEFAULT 60,
            min_trust_score INTEGER NOT NULL DEFAULT 60,
            last_checked_at TEXT
        );
        CREATE TABLE IF NOT EXISTS watch_alerts (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            saved_search_id INTEGER NOT NULL REFERENCES saved_searches(id) ON DELETE CASCADE,
            platform_listing_id TEXT NOT NULL,
            title TEXT NOT NULL,
            price REAL NOT NULL,
            currency TEXT NOT NULL DEFAULT 'USD',
            trust_score INTEGER NOT NULL,
            url TEXT,
            first_alerted_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
            dismissed_at TEXT,
            UNIQUE(saved_search_id, platform_listing_id)
        );
        INSERT INTO saved_searches (name, query, monitor_enabled) VALUES ('RTX 4090', 'rtx 4090', 1);
    """)
    conn.commit()
    conn.close()


@pytest.fixture
def monitor_db(tmp_path: Path) -> Path:
    db = tmp_path / "snipe.db"
    _create_monitor_db(db)
    return db


class TestStoreAlertMethods:
    def test_upsert_alert_new(self, monitor_db: Path):
        from app.db.models import WatchAlert
        from app.db.store import Store

        store = Store(monitor_db)
        alert = WatchAlert(
            saved_search_id=1, platform_listing_id="ebay-001",
            title="RTX 4090", price=750.0, trust_score=72, currency="USD",
            url="https://ebay.com/itm/001",
        )
        alert_id, is_new = store.upsert_alert(alert)
        assert is_new is True
        assert alert_id > 0

    def test_upsert_alert_dedup(self, monitor_db: Path):
        from app.db.models import WatchAlert
        from app.db.store import Store

        store = Store(monitor_db)
        alert = WatchAlert(
            saved_search_id=1, platform_listing_id="ebay-002",
            title="RTX 4090 FE", price=800.0, trust_score=68,
        )
        id1, new1 = store.upsert_alert(alert)
        id2, new2 = store.upsert_alert(alert)
        assert id1 == id2
        assert new1 is True
        assert new2 is False

    def test_list_alerts_returns_undismissed(self, monitor_db: Path):
        from app.db.models import WatchAlert
        from app.db.store import Store

        store = Store(monitor_db)
        alert = WatchAlert(
            saved_search_id=1, platform_listing_id="ebay-003",
            title="Test listing", price=500.0, trust_score=75,
        )
        store.upsert_alert(alert)
        alerts = store.list_alerts(include_dismissed=False)
        assert len(alerts) == 1
        assert alerts[0].platform_listing_id == "ebay-003"

    def test_count_undismissed_alerts(self, monitor_db: Path):
        from app.db.models import WatchAlert
        from app.db.store import Store

        store = Store(monitor_db)
        for i in range(3):
            store.upsert_alert(WatchAlert(
                saved_search_id=1, platform_listing_id=f"ebay-{i:03d}",
                title=f"Listing {i}", price=float(100 + i), trust_score=70,
            ))
        assert store.count_undismissed_alerts() == 3

    def test_dismiss_alert(self, monitor_db: Path):
        from app.db.models import WatchAlert
        from app.db.store import Store

        store = Store(monitor_db)
        alert = WatchAlert(
            saved_search_id=1, platform_listing_id="ebay-dismiss",
            title="To dismiss", price=400.0, trust_score=65,
        )
        alert_id, _ = store.upsert_alert(alert)
        store.dismiss_alert(alert_id)
        alerts = store.list_alerts(include_dismissed=False)
        assert all(a.id != alert_id for a in alerts)

    def test_dismiss_all_alerts(self, monitor_db: Path):
        from app.db.models import WatchAlert
        from app.db.store import Store

        store = Store(monitor_db)
        for i in range(3):
            store.upsert_alert(WatchAlert(
                saved_search_id=1, platform_listing_id=f"all-{i}",
                title=f"All {i}", price=float(100 * i), trust_score=70,
            ))
        count = store.dismiss_all_alerts()
        assert count == 3
        assert store.count_undismissed_alerts() == 0

    def test_mark_search_checked_updates_timestamp(self, monitor_db: Path):
        from app.db.store import Store

        store = Store(monitor_db)
        store.mark_search_checked(1)
        searches = store.list_monitored_searches()
        assert searches[0].last_checked_at is not None


# ---------------------------------------------------------------------------
# run_monitor_search — mocked adapter + trust aggregator
# ---------------------------------------------------------------------------


class TestRunMonitorSearch:
    def test_new_qualifying_listing_creates_alert(self, monitor_db: Path):
        from app.db.models import Listing, SavedSearch, TrustScore
        from app.db.store import Store
        from app.tasks.monitor import run_monitor_search

        search = SavedSearch(
            id=1, name="RTX 4090", query="rtx 4090",
            platform="ebay", monitor_enabled=True,
            min_trust_score=60,
        )
        mock_listing = Listing(
            platform="ebay", platform_listing_id="ebay-new",
            title="ASUS RTX 4090", price=750.0, currency="USD",
            condition="used", url="https://ebay.com/itm/new",
            buying_format="fixed_price", seller_platform_id="seller123",
        )
        mock_trust = TrustScore(
            listing_id=0, composite_score=72, score_is_partial=False,
            account_age_score=0, feedback_count_score=0, feedback_ratio_score=0,
            price_vs_market_score=0, category_history_score=0,
        )

        with patch("app.platforms.ebay.adapter.EbayAdapter") as MockAdapter, \
             patch("app.trust.TrustScorer") as MockAgg:
            MockAdapter.return_value.search.return_value = [mock_listing]
            MockAgg.return_value.score_batch.return_value = [mock_trust]

            count = run_monitor_search(search, user_db=monitor_db, shared_db=monitor_db)

        assert count == 1
        alerts = Store(monitor_db).list_alerts()
        assert len(alerts) == 1
        assert alerts[0].platform_listing_id == "ebay-new"

    def test_below_threshold_listing_not_alerted(self, monitor_db: Path):
        from app.db.models import Listing, SavedSearch, TrustScore
        from app.tasks.monitor import run_monitor_search

        search = SavedSearch(
            id=1, name="RTX 4090", query="rtx 4090",
            platform="ebay", monitor_enabled=True,
            min_trust_score=70,
        )
        mock_listing = Listing(
            platform="ebay", platform_listing_id="ebay-low",
            title="Sketchy RTX 4090", price=500.0, currency="USD",
            condition="used", url="https://ebay.com/itm/low",
            buying_format="fixed_price", seller_platform_id="s1",
        )
        mock_trust = TrustScore(
            listing_id=0, composite_score=55, score_is_partial=False,
            account_age_score=0, feedback_count_score=0, feedback_ratio_score=0,
            price_vs_market_score=0, category_history_score=0,
        )

        with patch("app.platforms.ebay.adapter.EbayAdapter") as MockAdapter, \
             patch("app.trust.TrustScorer") as MockAgg:
            MockAdapter.return_value.search.return_value = [mock_listing]
            MockAgg.return_value.score_batch.return_value = [mock_trust]

            count = run_monitor_search(search, user_db=monitor_db, shared_db=monitor_db)

        assert count == 0

    def test_duplicate_listing_not_double_alerted(self, monitor_db: Path):
        from app.db.models import Listing, SavedSearch, TrustScore
        from app.tasks.monitor import run_monitor_search

        search = SavedSearch(
            id=1, name="RTX 4090", query="rtx 4090",
            platform="ebay", monitor_enabled=True, min_trust_score=60,
        )
        mock_listing = Listing(
            platform="ebay", platform_listing_id="ebay-dupe",
            title="RTX 4090", price=700.0, currency="USD",
            condition="used", url="https://ebay.com/itm/dupe",
            buying_format="fixed_price", seller_platform_id="s1",
        )
        mock_trust = TrustScore(
            listing_id=0, composite_score=75, score_is_partial=False,
            account_age_score=0, feedback_count_score=0, feedback_ratio_score=0,
            price_vs_market_score=0, category_history_score=0,
        )

        with patch("app.platforms.ebay.adapter.EbayAdapter") as MockAdapter, \
             patch("app.trust.TrustScorer") as MockAgg:
            MockAdapter.return_value.search.return_value = [mock_listing]
            MockAgg.return_value.score_batch.return_value = [mock_trust]

            count1 = run_monitor_search(search, user_db=monitor_db, shared_db=monitor_db)
            count2 = run_monitor_search(search, user_db=monitor_db, shared_db=monitor_db)

        assert count1 == 1
        assert count2 == 0  # deduped by UNIQUE constraint

    def test_adapter_failure_returns_zero(self, monitor_db: Path):
        from app.db.models import SavedSearch
        from app.tasks.monitor import run_monitor_search

        search = SavedSearch(
            id=1, name="RTX 4090", query="rtx 4090",
            platform="ebay", monitor_enabled=True, min_trust_score=60,
        )

        with patch("app.platforms.ebay.adapter.EbayAdapter") as MockAdapter:
            MockAdapter.return_value.search.side_effect = RuntimeError("eBay down")
            count = run_monitor_search(search, user_db=monitor_db, shared_db=monitor_db)

        assert count == 0
