"""
Tests for Phase 33: Full-Text Search & Discovery (platform_search).
"""

import json
import time
from datetime import datetime, timezone, timedelta

import pytest


# ── Fixture: isolate all dirs to tmp_path ──────────────────────────────────


@pytest.fixture(autouse=True)
def _isolate(tmp_path, monkeypatch):
    """Redirect every module-level directory to a fresh temp tree."""
    profiles = tmp_path / "profiles"
    tasks = tmp_path / "tasks"
    channels = tmp_path / "channels"
    sessions = tmp_path / "sessions"
    jobs = tmp_path / "jobs"
    services = tmp_path / "marketplace" / "services"
    social_posts = tmp_path / "social" / "posts"

    for d in (profiles, tasks, channels, sessions, jobs, services, social_posts):
        d.mkdir(parents=True, exist_ok=True)

    # Patch brynq.server paths first (they are imported by platform_search)
    import brynq.server as srv
    monkeypatch.setattr(srv, "BROKER_DIR", tmp_path)
    monkeypatch.setattr(srv, "CHANNELS_DIR", channels)
    monkeypatch.setattr(srv, "SESSIONS_DIR", sessions)

    # Now patch platform_search module-level dirs
    import brynq.platform_search as ps
    monkeypatch.setattr(ps, "PROFILES_DIR", profiles)
    monkeypatch.setattr(ps, "TASKS_DIR", tasks)
    monkeypatch.setattr(ps, "JOBS_DIR", jobs)
    monkeypatch.setattr(ps, "SERVICES_DIR", services)
    monkeypatch.setattr(ps, "SOCIAL_POSTS_DIR", social_posts)
    monkeypatch.setattr(ps, "CHANNELS_DIR", channels)

    # Clear the inverted index between tests
    ps._index.clear()


# ── Helpers: write sample data ─────────────────────────────────────────────


def _write_profile(tmp_path, sid, name, bio, skills, **extra):
    p = {
        "session_id": sid,
        "session_name": name,
        "platform": extra.get("platform", "claude"),
        "bio": bio,
        "skills": skills,
        "availability": extra.get("availability", "available"),
        "reputation": extra.get("reputation", 5.0),
        "tasks_completed": extra.get("tasks_completed", 0),
        "tasks_failed": 0,
        "total_ratings": 0,
        "rating_sum": 0.0,
        "created": extra.get("created", datetime.now(timezone.utc).isoformat()),
        "updated": extra.get("updated", datetime.now(timezone.utc).isoformat()),
    }
    (tmp_path / "profiles" / f"{sid}.json").write_text(json.dumps(p))


def _write_task(tmp_path, task_id, title, description, status="pending", **extra):
    t = {
        "task_id": task_id,
        "channel": extra.get("channel", "general"),
        "status": status,
        "title": title,
        "description": description,
        "requester_name": extra.get("requester", "bot1"),
        "created": extra.get("created", datetime.now(timezone.utc).isoformat()),
    }
    (tmp_path / "tasks" / f"{task_id}.json").write_text(json.dumps(t))


def _write_message(tmp_path, channel, msg_id, sender, message, **extra):
    entry = {
        "id": msg_id,
        "timestamp": extra.get("timestamp", datetime.now(timezone.utc).isoformat()),
        "session_id": extra.get("session_id", "s1"),
        "session_name": sender,
        "platform": extra.get("platform", "claude"),
        "channel": channel,
        "msg_type": extra.get("msg_type", "info"),
        "message": message,
    }
    path = tmp_path / "channels" / f"{channel}.jsonl"
    with open(path, "a", encoding="utf-8") as f:
        f.write(json.dumps(entry) + "\n")


def _write_job(tmp_path, job_id, title, description, skills, status="open", **extra):
    j = {
        "job_id": job_id,
        "title": title,
        "description": description,
        "required_skills": skills,
        "preferred_skills": extra.get("preferred_skills", []),
        "status": status,
        "posted_by": extra.get("posted_by", "poster1"),
        "created": extra.get("created", datetime.now(timezone.utc).isoformat()),
    }
    (tmp_path / "jobs" / f"{job_id}.json").write_text(json.dumps(j))


def _write_service(tmp_path, service_id, title, description, category, **extra):
    s = {
        "service_id": service_id,
        "title": title,
        "description": description,
        "category": category,
        "status": extra.get("status", "active"),
        "provider_name": extra.get("provider", "provider1"),
        "created": extra.get("created", datetime.now(timezone.utc).isoformat()),
    }
    (tmp_path / "marketplace" / "services" / f"{service_id}.json").write_text(json.dumps(s))


def _write_social_post(tmp_path, post_id, author_name, content, **extra):
    sp = {
        "post_id": post_id,
        "author_id": extra.get("author_id", "a1"),
        "author_name": author_name,
        "content": content,
        "tags": extra.get("tags", []),
        "created_at": extra.get("created_at", datetime.now(timezone.utc).isoformat()),
    }
    (tmp_path / "social" / "posts" / f"{post_id}.json").write_text(json.dumps(sp))


# ── Tests: Tokenizer ──────────────────────────────────────────────────────


class TestTokenizer:
    def test_basic_tokenize(self):
        from brynq.platform_search import _tokenize
        tokens = _tokenize("Hello World! Python developer")
        assert "hello" in tokens
        assert "world" in tokens
        assert "python" in tokens
        assert "developer" in tokens

    def test_stopwords_removed(self):
        from brynq.platform_search import _tokenize
        tokens = _tokenize("this is a test for the module")
        assert "this" not in tokens
        assert "test" in tokens
        assert "module" in tokens

    def test_empty_string(self):
        from brynq.platform_search import _tokenize
        assert _tokenize("") == []
        assert _tokenize(None) == []


# ── Tests: Unified Search ─────────────────────────────────────────────────


class TestUnifiedSearch:
    def test_search_across_agents(self, tmp_path):
        _write_profile(tmp_path, "a1", "CodeBot", "Expert Python developer", ["python", "testing"])
        from brynq.platform_search import search
        results = search("python")
        assert len(results) >= 1
        assert any(r["type"] == "agent" for r in results)

    def test_search_across_tasks(self, tmp_path):
        _write_task(tmp_path, "t1", "Fix Python tests", "Repair broken unit tests in the python module")
        from brynq.platform_search import search
        results = search("python")
        assert any(r["type"] == "task" for r in results)

    def test_search_across_messages(self, tmp_path):
        _write_message(tmp_path, "general", "m1", "Bot1", "Finished the python migration today")
        from brynq.platform_search import search
        results = search("python")
        assert any(r["type"] == "message" for r in results)

    def test_search_across_jobs(self, tmp_path):
        _write_job(tmp_path, "j1", "Python Developer Needed", "We need a python expert", ["python"])
        from brynq.platform_search import search
        results = search("python")
        assert any(r["type"] == "job" for r in results)

    def test_search_across_services(self, tmp_path):
        _write_service(tmp_path, "s1", "Python Code Review", "Review python code quality", "code_review")
        from brynq.platform_search import search
        results = search("python")
        assert any(r["type"] == "service" for r in results)

    def test_search_across_social_posts(self, tmp_path):
        _write_social_post(tmp_path, "sp1", "DevBot", "Just mastered python decorators!")
        from brynq.platform_search import search
        results = search("python")
        assert any(r["type"] == "social_post" for r in results)

    def test_search_all_types_at_once(self, tmp_path):
        _write_profile(tmp_path, "a1", "PyBot", "Python expert", ["python"])
        _write_task(tmp_path, "t1", "Python task", "Do python stuff")
        _write_message(tmp_path, "dev", "m1", "Bot1", "python release ready")
        _write_job(tmp_path, "j1", "Python role", "Need python dev", ["python"])
        _write_service(tmp_path, "s1", "Python audit", "Audit python code", "code_review")
        _write_social_post(tmp_path, "sp1", "Agent1", "Learning python today")

        from brynq.platform_search import search
        results = search("python")
        types_found = {r["type"] for r in results}
        assert "agent" in types_found
        assert "task" in types_found
        assert "message" in types_found
        assert "job" in types_found
        assert "service" in types_found
        assert "social_post" in types_found

    def test_search_with_type_filter(self, tmp_path):
        _write_profile(tmp_path, "a1", "PyBot", "Python expert", ["python"])
        _write_task(tmp_path, "t1", "Python task", "Do python stuff")
        from brynq.platform_search import search
        results = search("python", types=["agent"])
        assert all(r["type"] == "agent" for r in results)
        assert len(results) >= 1

    def test_search_limit(self, tmp_path):
        for i in range(10):
            _write_task(tmp_path, f"t{i}", f"Python task {i}", "Python related work")
        from brynq.platform_search import search
        results = search("python", limit=3)
        assert len(results) == 3

    def test_search_result_structure(self, tmp_path):
        _write_task(tmp_path, "t1", "Build API", "Create the REST API endpoints")
        from brynq.platform_search import search
        results = search("api")
        assert len(results) >= 1
        r = results[0]
        assert "type" in r
        assert "id" in r
        assert "title" in r
        assert "snippet" in r
        assert "score" in r
        assert "timestamp" in r


# ── Tests: Empty Query ─────────────────────────────────────────────────────


class TestEmptyQuery:
    def test_empty_string_returns_empty(self):
        from brynq.platform_search import search
        assert search("") == []

    def test_whitespace_returns_empty(self):
        from brynq.platform_search import search
        assert search("   ") == []

    def test_stopword_only_returns_empty(self):
        from brynq.platform_search import search
        # "the" and "a" are stop words; after tokenization no tokens remain
        assert search("the a") == []


# ── Tests: Type-Specific Searches ─────────────────────────────────────────


class TestSearchAgents:
    def test_search_by_skill(self, tmp_path):
        _write_profile(tmp_path, "a1", "Bot1", "I do things", ["rust", "go"])
        _write_profile(tmp_path, "a2", "Bot2", "Backend dev", ["python", "go"])
        from brynq.platform_search import search_agents
        results = search_agents("rust")
        assert len(results) >= 1
        assert results[0]["session_id"] == "a1"

    def test_filter_platform(self, tmp_path):
        _write_profile(tmp_path, "a1", "Bot1", "Developer", ["python"], platform="claude")
        _write_profile(tmp_path, "a2", "Bot2", "Developer", ["python"], platform="gemini")
        from brynq.platform_search import search_agents
        results = search_agents("developer", filters={"platform": "claude"})
        assert all(r["platform"] == "claude" for r in results)

    def test_filter_availability(self, tmp_path):
        _write_profile(tmp_path, "a1", "Bot1", "Busy dev", ["python"], availability="busy")
        _write_profile(tmp_path, "a2", "Bot2", "Free dev", ["python"], availability="available")
        from brynq.platform_search import search_agents
        results = search_agents("dev", filters={"availability": "available"})
        assert all(r["availability"] == "available" for r in results)


class TestSearchTasks:
    def test_search_by_title(self, tmp_path):
        _write_task(tmp_path, "t1", "Deploy microservices", "Ship the k8s deploy")
        _write_task(tmp_path, "t2", "Write documentation", "Document the API")
        from brynq.platform_search import search_tasks
        results = search_tasks("microservices")
        assert len(results) >= 1
        assert results[0]["task_id"] == "t1"

    def test_filter_by_status(self, tmp_path):
        _write_task(tmp_path, "t1", "Pending deploy", "Deploy stuff", status="pending")
        _write_task(tmp_path, "t2", "Completed deploy", "Deployed stuff", status="completed")
        from brynq.platform_search import search_tasks
        results = search_tasks("deploy", status="pending")
        assert all(r["status"] == "pending" for r in results)


class TestSearchMessages:
    def test_search_message_content(self, tmp_path):
        _write_message(tmp_path, "dev", "m1", "Bot1", "Database migration completed successfully")
        _write_message(tmp_path, "dev", "m2", "Bot2", "Frontend build is passing")
        from brynq.platform_search import search_messages
        results = search_messages("database")
        assert len(results) >= 1
        assert "database" in results[0]["message"].lower()

    def test_filter_by_channel(self, tmp_path):
        _write_message(tmp_path, "dev", "m1", "Bot1", "Deploying code now")
        _write_message(tmp_path, "ops", "m2", "Bot2", "Deploying infra changes")
        from brynq.platform_search import search_messages
        results = search_messages("deploying", channel="dev")
        assert all(r["channel"] == "dev" for r in results)

    def test_filter_by_sender(self, tmp_path):
        _write_message(tmp_path, "dev", "m1", "Alice", "Server restarted")
        _write_message(tmp_path, "dev", "m2", "Bob", "Server checked")
        from brynq.platform_search import search_messages
        results = search_messages("server", sender="Alice")
        assert all("alice" in r["session_name"].lower() for r in results)


class TestSearchJobs:
    def test_search_by_title(self, tmp_path):
        _write_job(tmp_path, "j1", "Senior Rust Developer", "Build systems code", ["rust"])
        _write_job(tmp_path, "j2", "Frontend Engineer", "React components", ["react"])
        from brynq.platform_search import search_jobs
        results = search_jobs("rust")
        assert len(results) >= 1
        assert results[0]["job_id"] == "j1"

    def test_filter_by_status(self, tmp_path):
        _write_job(tmp_path, "j1", "Backend Dev", "Python backend", ["python"], status="open")
        _write_job(tmp_path, "j2", "Backend Lead", "Python lead", ["python"], status="closed")
        from brynq.platform_search import search_jobs
        results = search_jobs("python", status="open")
        assert all(r["status"] == "open" for r in results)

    def test_filter_by_skill(self, tmp_path):
        _write_job(tmp_path, "j1", "Dev Role", "Developer needed", ["python", "go"])
        _write_job(tmp_path, "j2", "Dev Role", "Developer needed", ["rust", "go"])
        from brynq.platform_search import search_jobs
        results = search_jobs("developer", skill="python")
        assert len(results) >= 1
        assert results[0]["job_id"] == "j1"


class TestSearchServices:
    def test_search_by_title(self, tmp_path):
        _write_service(tmp_path, "s1", "Security Audit Pro", "Deep security review", "security_audit")
        _write_service(tmp_path, "s2", "Code Formatter", "Format and lint code", "code_review")
        from brynq.platform_search import search_services
        results = search_services("security")
        assert len(results) >= 1
        assert results[0]["service_id"] == "s1"

    def test_filter_by_category(self, tmp_path):
        _write_service(tmp_path, "s1", "Review Code", "Code review service", "code_review")
        _write_service(tmp_path, "s2", "Debug Code", "Debugging service", "debugging")
        from brynq.platform_search import search_services
        results = search_services("code", category="code_review")
        assert all(r["category"] == "code_review" for r in results)


# ── Tests: Scoring ─────────────────────────────────────────────────────────


class TestScoring:
    def test_title_match_scores_higher(self, tmp_path):
        """A task with the query in its title should score higher than one
        with the query only in the description."""
        _write_task(tmp_path, "t_title", "Kubernetes deployment", "Set up the cluster")
        _write_task(tmp_path, "t_body", "Infrastructure work", "Handle kubernetes cluster setup")

        from brynq.platform_search import search
        results = search("kubernetes", types=["task"])
        assert len(results) == 2
        # The one with "kubernetes" in the title should be first
        assert results[0]["id"] == "t_title"
        assert results[0]["score"] > results[1]["score"]

    def test_exact_match_bonus(self, tmp_path):
        """A multi-word exact match in the title should score higher."""
        _write_task(tmp_path, "t_exact", "Code review automation", "Automate the review pipeline")
        _write_task(tmp_path, "t_partial", "Review some code later", "Automation for other things")

        from brynq.platform_search import search
        results = search("code review", types=["task"])
        assert len(results) >= 1
        # t_exact has "code review" as an exact substring in the title
        assert results[0]["id"] == "t_exact"

    def test_scores_are_positive(self, tmp_path):
        _write_task(tmp_path, "t1", "Python stuff", "Working on python")
        from brynq.platform_search import search
        results = search("python")
        assert all(r["score"] > 0 for r in results)


# ── Tests: Trending & Popular ──────────────────────────────────────────────


class TestTrending:
    def test_trending_topics(self, tmp_path):
        now = datetime.now(timezone.utc).isoformat()
        for i in range(5):
            _write_message(tmp_path, "dev", f"m{i}", "Bot1",
                           f"Working on kubernetes deployment {i}", timestamp=now)
        _write_message(tmp_path, "dev", "m99", "Bot2",
                       "Unrelated message about nothing", timestamp=now)

        from brynq.platform_search import get_trending
        trending = get_trending(hours=24, limit=5)
        assert len(trending) >= 1
        # "kubernetes" or "deployment" should be in the top topics
        topics = [t["topic"] for t in trending]
        assert "kubernetes" in topics or "deployment" in topics

    def test_trending_empty(self, tmp_path):
        from brynq.platform_search import get_trending
        assert get_trending(hours=24) == []

    def test_trending_respects_time_window(self, tmp_path):
        old_time = (datetime.now(timezone.utc) - timedelta(hours=48)).isoformat()
        _write_message(tmp_path, "dev", "m1", "Bot1",
                       "Old kubernetes message", timestamp=old_time)
        from brynq.platform_search import get_trending
        trending = get_trending(hours=24)
        topics = [t["topic"] for t in trending]
        assert "kubernetes" not in topics


class TestPopularAgents:
    def test_popular_agents_ranking(self, tmp_path):
        _write_profile(tmp_path, "a1", "NoviceBot", "New here", ["python"],
                       reputation=3.0, tasks_completed=1)
        _write_profile(tmp_path, "a2", "ExpertBot", "Seasoned agent", ["python", "rust"],
                       reputation=4.8, tasks_completed=50)
        from brynq.platform_search import get_popular_agents
        results = get_popular_agents(limit=10)
        assert len(results) == 2
        assert results[0]["session_id"] == "a2"
        assert results[0]["popularity_score"] > results[1]["popularity_score"]

    def test_popular_agents_empty(self, tmp_path):
        from brynq.platform_search import get_popular_agents
        assert get_popular_agents() == []


# ── Tests: Recent Activity ─────────────────────────────────────────────────


class TestRecentActivity:
    def test_activity_feed(self, tmp_path):
        now = datetime.now(timezone.utc).isoformat()
        _write_task(tmp_path, "t1", "Build API", "REST API", created=now)
        _write_message(tmp_path, "dev", "m1", "Bot1", "Deployed", timestamp=now)
        _write_job(tmp_path, "j1", "Dev job", "Need dev", ["python"], created=now)

        from brynq.platform_search import get_recent_activity
        feed = get_recent_activity(limit=10)
        types_found = {a["type"] for a in feed}
        assert "task" in types_found
        assert "message" in types_found
        assert "job" in types_found

    def test_activity_feed_respects_limit(self, tmp_path):
        now = datetime.now(timezone.utc).isoformat()
        for i in range(15):
            _write_task(tmp_path, f"t{i}", f"Task {i}", "Desc", created=now)
        from brynq.platform_search import get_recent_activity
        feed = get_recent_activity(limit=5)
        assert len(feed) == 5


# ── Tests: Autocomplete ───────────────────────────────────────────────────


class TestAutocomplete:
    def test_autocomplete_agent_name(self, tmp_path):
        _write_profile(tmp_path, "a1", "CodeMaster", "Expert coder", ["python"])
        from brynq.platform_search import autocomplete
        results = autocomplete("Code")
        assert "CodeMaster" in results

    def test_autocomplete_skill(self, tmp_path):
        _write_profile(tmp_path, "a1", "Bot1", "Dev", ["python", "pytorch"])
        from brynq.platform_search import autocomplete
        results = autocomplete("py")
        assert any("py" in r.lower() for r in results)

    def test_autocomplete_empty_prefix(self):
        from brynq.platform_search import autocomplete
        assert autocomplete("") == []
        assert autocomplete("   ") == []

    def test_autocomplete_limit(self, tmp_path):
        for i in range(20):
            _write_profile(tmp_path, f"a{i}", f"Agent{i}", "Dev", [f"skill{i}"])
        from brynq.platform_search import autocomplete
        results = autocomplete("Agent", limit=3)
        assert len(results) <= 3

    def test_autocomplete_from_jobs(self, tmp_path):
        _write_job(tmp_path, "j1", "Kubernetes Engineer", "K8s stuff", ["kubernetes"])
        from brynq.platform_search import autocomplete
        results = autocomplete("Kub")
        assert any("Kubernetes" in r or "kubernetes" in r for r in results)


# ── Tests: Index Building ─────────────────────────────────────────────────


class TestIndexBuilding:
    def test_build_index_counts(self, tmp_path):
        _write_profile(tmp_path, "a1", "Bot1", "Dev", ["python"])
        _write_task(tmp_path, "t1", "Task", "Do something")
        _write_job(tmp_path, "j1", "Job", "Need help", ["go"])
        _write_service(tmp_path, "s1", "Svc", "Service desc", "testing")
        _write_social_post(tmp_path, "sp1", "Agent1", "Hello world")
        _write_message(tmp_path, "dev", "m1", "Bot1", "Testing 123")

        from brynq.platform_search import build_index
        summary = build_index()
        assert summary["indexed"]["agents"] == 1
        assert summary["indexed"]["tasks"] == 1
        assert summary["indexed"]["jobs"] == 1
        assert summary["indexed"]["services"] == 1
        assert summary["indexed"]["social_posts"] == 1
        assert summary["indexed"]["messages"] == 1
        assert summary["built_at"] > 0

    def test_build_index_empty(self, tmp_path):
        from brynq.platform_search import build_index
        summary = build_index()
        assert all(v == 0 for v in summary["indexed"].values())

    def test_index_autocomplete_integration(self, tmp_path):
        _write_profile(tmp_path, "a1", "RustExpert", "Rust systems dev", ["rust"])
        _write_task(tmp_path, "t1", "Rust compiler bug", "Fix the rust compiler issue")

        from brynq.platform_search import build_index, autocomplete
        build_index()
        results = autocomplete("rus")
        assert any("rust" in r.lower() for r in results)


# ── Tests: No Results Case ────────────────────────────────────────────────


class TestNoResults:
    def test_no_results_unified(self, tmp_path):
        _write_task(tmp_path, "t1", "Python stuff", "Working on python")
        from brynq.platform_search import search
        results = search("zzzznonexistent")
        assert results == []

    def test_no_results_agents(self, tmp_path):
        from brynq.platform_search import search_agents
        assert search_agents("nonexistent") == []

    def test_no_results_tasks(self, tmp_path):
        from brynq.platform_search import search_tasks
        assert search_tasks("nonexistent") == []

    def test_no_results_messages(self, tmp_path):
        from brynq.platform_search import search_messages
        assert search_messages("nonexistent") == []

    def test_no_results_jobs(self, tmp_path):
        from brynq.platform_search import search_jobs
        assert search_jobs("nonexistent") == []

    def test_no_results_services(self, tmp_path):
        from brynq.platform_search import search_services
        assert search_services("nonexistent") == []


# ── Tests: Snippet Extraction ─────────────────────────────────────────────


class TestSnippet:
    def test_snippet_contains_query(self):
        from brynq.platform_search import _extract_snippet
        text = "This is a long document about machine learning and neural networks for production systems"
        snippet = _extract_snippet(text, "neural")
        assert "neural" in snippet.lower()

    def test_snippet_empty_text(self):
        from brynq.platform_search import _extract_snippet
        assert _extract_snippet("", "query") == ""

    def test_snippet_no_match_returns_start(self):
        from brynq.platform_search import _extract_snippet
        text = "Some text about topics"
        snippet = _extract_snippet(text, "zzz")
        assert snippet.startswith("Some")
