"""Smart Search & Discovery tests (Phase 1)."""

import os
import unittest
from decimal import Decimal

os.environ["TEST_DATABASE_URL"] = "sqlite:///:memory:"

from werkzeug.security import generate_password_hash

from app import create_app
from app.extensions import db
from app.models import (
    Category,
    Listing,
    ListingCondition,
    ListingStatus,
    ListingVisibility,
    ReviewStatus,
    User,
    UserProfile,
    UserReputation,
)
from app.marketplace.services import get_search_suggestions, search_listings


class SearchTests(unittest.TestCase):
    def setUp(self):
        self.app = create_app("testing")
        self.ctx = self.app.app_context()
        self.ctx.push()
        db.create_all()
        self.cat = Category(name="Electronics", description="d", is_active=True, display_order=1)
        db.session.add(self.cat)
        db.session.commit()

    def tearDown(self):
        db.session.remove()
        db.drop_all()
        self.ctx.pop()

    # --- factories ---
    def _user(self, email, name=None):
        u = User(is_verified=True, email=email, password_hash=generate_password_hash("Passw0rd!"))
        db.session.add(u)
        db.session.commit()
        if name:
            db.session.add(UserProfile(user_id=u.id, full_name=name))
            db.session.commit()
        return u

    def _rep(self, user, **kwargs):
        db.session.add(UserReputation(user_id=user.id, **kwargs))
        db.session.commit()

    def _listing(self, seller, title, price="50", *, condition=ListingCondition.GOOD,
                 category=None, view_count=0, favorite_count=0, description="A campus item", idx=None):
        idx = idx if idx is not None else db.session.scalar(db.select(db.func.count(Listing.id))) + 1
        listing = Listing(
            sku=f"SKU-{seller.id}-{idx}", seller_id=seller.id, category_id=(category or self.cat).id,
            title=title, slug=f"slug-{seller.id}-{idx}", description=description, price=Decimal(str(price)),
            condition=condition, status=ListingStatus.AVAILABLE, visibility=ListingVisibility.PUBLIC,
            review_status=ReviewStatus.APPROVED, view_count=view_count, favorite_count=favorite_count,
        )
        db.session.add(listing)
        db.session.commit()
        return listing

    def _titles(self, pagination):
        return [l.title for l in pagination.items]

    # --- advanced search ---
    def test_search_matches_title_description_category_seller(self):
        seller = self._user("jane@c.edu", name="Jane Doe")
        self._listing(seller, "MacBook Air", description="lightweight laptop")
        self._listing(seller, "Notebook", description="mentions macbook inside", idx=2)
        cat2 = Category(name="MacBook Accessories", description="d", is_active=True, display_order=2)
        db.session.add(cat2); db.session.commit()
        self._listing(seller, "Charger", category=cat2, idx=3)
        other = self._user("mac.seller@c.edu", name="Macy Seller")
        self._listing(other, "Random Item", idx=4)
        # "mac" hits: title, description, category name and seller name (case-insensitive)
        titles = self._titles(search_listings(1, q="mac"))
        self.assertIn("MacBook Air", titles)
        self.assertIn("Notebook", titles)          # description match
        self.assertIn("Charger", titles)           # category-name match
        self.assertIn("Random Item", titles)       # seller-name match ("Macy")

    # --- price range ---
    def test_price_range_filter(self):
        s = self._user("s@c.edu")
        self._listing(s, "Cheap", price="50")
        self._listing(s, "Mid", price="300", idx=2)
        self._listing(s, "Pricey", price="900", idx=3)
        titles = self._titles(search_listings(1, min_price=Decimal("100"), max_price=Decimal("500")))
        self.assertEqual(titles, ["Mid"])

    def test_inverted_price_range_is_corrected(self):
        s = self._user("s@c.edu")
        self._listing(s, "Mid", price="300")
        # min > max is swapped rather than returning nothing
        self.assertEqual(len(search_listings(1, min_price=Decimal("500"), max_price=Decimal("100")).items), 1)

    # --- condition ---
    def test_condition_filter(self):
        s = self._user("s@c.edu")
        self._listing(s, "New one", condition=ListingCondition.NEW)
        self._listing(s, "Good one", condition=ListingCondition.GOOD, idx=2)
        self.assertEqual(self._titles(search_listings(1, condition="new")), ["New one"])

    # --- seller rating (cached reputation) ---
    def test_rating_filter_uses_cached_reputation(self):
        good = self._user("good@c.edu")
        self._rep(good, average_rating=Decimal("4.7"), review_count=10)
        mid = self._user("mid@c.edu")
        self._rep(mid, average_rating=Decimal("4.2"), review_count=10)
        new = self._user("new@c.edu")  # no reputation row
        self._listing(good, "Good seller item")
        self._listing(mid, "Mid seller item", idx=2)
        self._listing(new, "New seller item", idx=3)
        self.assertEqual(self._titles(search_listings(1, seller_rating="4.5")), ["Good seller item"])
        self.assertEqual(self._titles(search_listings(1, seller_rating="new")), ["New seller item"])

    # --- badge + trusted filters ---
    def test_badge_filter(self):
        top = self._user("top@c.edu")
        self._rep(top, average_rating=Decimal("4.9"), review_count=25, completed_sales=60)
        plain = self._user("plain@c.edu")
        self._rep(plain, average_rating=Decimal("4.0"), review_count=3, completed_sales=1)
        self._listing(top, "Top item")
        self._listing(plain, "Plain item", idx=2)
        self.assertEqual(self._titles(search_listings(1, badge="top_seller")), ["Top item"])

    def test_trusted_seller_filter(self):
        trusted = self._user("trusted@c.edu")
        self._rep(trusted, average_rating=Decimal("4.6"), review_count=6, completed_sales=12)
        plain = self._user("plain@c.edu")
        self._rep(plain, average_rating=Decimal("4.0"), review_count=2, completed_sales=1)
        self._listing(trusted, "Trusted item")
        self._listing(plain, "Plain item", idx=2)
        self.assertEqual(self._titles(search_listings(1, trusted_only=True)), ["Trusted item"])

    # --- sorting ---
    def test_sorting(self):
        s = self._user("s@c.edu")
        self._listing(s, "A", price="300", view_count=5)
        self._listing(s, "B", price="100", view_count=50, idx=2)
        self._listing(s, "C", price="200", view_count=20, idx=3)
        self.assertEqual(self._titles(search_listings(1, sort="price_low")), ["B", "C", "A"])
        self.assertEqual(self._titles(search_listings(1, sort="most_viewed")), ["B", "C", "A"])

    def test_best_rated_seller_sort(self):
        hi = self._user("hi@c.edu"); self._rep(hi, average_rating=Decimal("4.9"), review_count=10)
        lo = self._user("lo@c.edu"); self._rep(lo, average_rating=Decimal("3.1"), review_count=10)
        self._listing(lo, "Low item")
        self._listing(hi, "High item", idx=2)
        self.assertEqual(self._titles(search_listings(1, sort="best_rated_seller"))[0], "High item")

    # --- suggestions ---
    def test_suggestions_limited_and_prefix_ranked(self):
        s = self._user("s@c.edu")
        for i, t in enumerate(["MacBook", "MacBook Air", "MacBook Pro", "MacBook Mini",
                               "MacBook Max", "A Mac case"]):
            self._listing(s, t, idx=i + 1)
        sugg = get_search_suggestions("mac", limit=5)
        self.assertEqual(len(sugg), 5)                     # capped at five
        self.assertTrue(all("mac" in t.lower() for t in sugg))
        self.assertTrue(sugg[0].lower().startswith("mac"))  # prefix matches rank first

    def test_suggestions_ignore_short_queries(self):
        self.assertEqual(get_search_suggestions("m"), [])

    # --- pagination ---
    def test_pagination_maintained(self):
        s = self._user("s@c.edu")
        per_page = self.app.config.get("DEFAULT_PAGE_SIZE", 20)
        for i in range(per_page + 3):
            self._listing(s, f"Item {i}", idx=i + 1)
        page1 = search_listings(1)
        page2 = search_listings(2)
        self.assertEqual(len(page1.items), per_page)
        self.assertEqual(len(page2.items), 3)
        self.assertEqual(page1.total, per_page + 3)

    # --- empty state (route) ---
    def test_empty_state_rendered_for_no_matches(self):
        html = self.app.test_client().get("/marketplace?q=zzznomatchzzz").get_data(as_text=True)
        self.assertIn("No matching listings found.", html)
        self.assertIn("Clear Filters", html)


if __name__ == "__main__":
    unittest.main()
