"""
tests/track_a/test_story_5_09.py

Story 5.09 — GenesisTaskWorker: Genesis Task Bridge

Black-box tests  (BB1–BB3): validate external behaviour via public API.
White-box tests  (WB1–WB3): validate internal implementation details.

All tests use ``tmp_path`` — never write to the real loop/tasks.json.
No real I/O beyond the tmp_path fixture. No SQLite. No network.
"""

import sys
sys.path.insert(0, "/mnt/e/genesis-system")

import asyncio
import json
import pytest
from datetime import datetime
from pathlib import Path
from unittest.mock import MagicMock
from uuid import UUID

from core.workers.genesis_task_worker import GenesisTaskWorker
from core.intent.intent_signal import IntentType, IntentSignal


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _make_intent(
    session_id: str = "test-session-509",
    utterance: str = "please check the voicemail messages",
    task_entity: str | None = None,
) -> IntentSignal:
    """Factory: returns a TASK_DISPATCH IntentSignal."""
    entities: dict = {}
    if task_entity is not None:
        entities["task"] = task_entity
    return IntentSignal(
        session_id=session_id,
        utterance=utterance,
        intent_type=IntentType.TASK_DISPATCH,
        confidence=0.9,
        extracted_entities=entities,
        requires_swarm=True,
        created_at=datetime(2026, 2, 25, 12, 0, 0),
        raw_gemini_response=None,
    )


def _make_worker(tmp_path: Path) -> GenesisTaskWorker:
    """Factory: creates a GenesisTaskWorker writing to tmp_path/tasks.json."""
    return GenesisTaskWorker(task_board_path=tmp_path / "tasks.json")


def _load_tasks(tmp_path: Path) -> list[dict]:
    """Helper: load tasks written by the worker in tmp_path."""
    tasks_file = tmp_path / "tasks.json"
    if not tasks_file.exists():
        return []
    return json.loads(tasks_file.read_text())


# ---------------------------------------------------------------------------
# BB1: TASK_DISPATCH intent → task appears in tasks list
# ---------------------------------------------------------------------------


class TestBB1_TaskAppearsInTasksList:
    """BB1 — execute() appends a task entry to the task board file."""

    @pytest.mark.asyncio
    async def test_task_board_file_created(self, tmp_path):
        worker = _make_worker(tmp_path)
        intent = _make_intent()
        await worker.execute(intent)
        assert (tmp_path / "tasks.json").exists()

    @pytest.mark.asyncio
    async def test_task_board_contains_one_entry(self, tmp_path):
        worker = _make_worker(tmp_path)
        intent = _make_intent()
        await worker.execute(intent)
        tasks = _load_tasks(tmp_path)
        assert len(tasks) == 1

    @pytest.mark.asyncio
    async def test_task_board_entry_is_dict(self, tmp_path):
        worker = _make_worker(tmp_path)
        intent = _make_intent()
        await worker.execute(intent)
        tasks = _load_tasks(tmp_path)
        assert isinstance(tasks[0], dict)

    @pytest.mark.asyncio
    async def test_task_has_id_field(self, tmp_path):
        worker = _make_worker(tmp_path)
        intent = _make_intent()
        await worker.execute(intent)
        tasks = _load_tasks(tmp_path)
        assert "id" in tasks[0]

    @pytest.mark.asyncio
    async def test_task_has_description_field(self, tmp_path):
        worker = _make_worker(tmp_path)
        intent = _make_intent()
        await worker.execute(intent)
        tasks = _load_tasks(tmp_path)
        assert "description" in tasks[0]
        assert tasks[0]["description"]  # non-empty

    @pytest.mark.asyncio
    async def test_task_has_status_pending(self, tmp_path):
        worker = _make_worker(tmp_path)
        intent = _make_intent()
        await worker.execute(intent)
        tasks = _load_tasks(tmp_path)
        assert tasks[0]["status"] == "pending"

    @pytest.mark.asyncio
    async def test_task_has_created_at_field(self, tmp_path):
        worker = _make_worker(tmp_path)
        intent = _make_intent()
        await worker.execute(intent)
        tasks = _load_tasks(tmp_path)
        assert "created_at" in tasks[0]
        assert "T" in tasks[0]["created_at"]  # ISO 8601 check


# ---------------------------------------------------------------------------
# BB2: Task has source="AIVA" field
# ---------------------------------------------------------------------------


class TestBB2_SourceIsAIVA:
    """BB2 — every task written by this worker is stamped source='AIVA'."""

    @pytest.mark.asyncio
    async def test_task_source_is_aiva(self, tmp_path):
        worker = _make_worker(tmp_path)
        intent = _make_intent()
        await worker.execute(intent)
        tasks = _load_tasks(tmp_path)
        assert tasks[0]["source"] == "AIVA"

    @pytest.mark.asyncio
    async def test_source_is_aiva_with_custom_utterance(self, tmp_path):
        worker = _make_worker(tmp_path)
        intent = _make_intent(utterance="send the weekly report")
        await worker.execute(intent)
        tasks = _load_tasks(tmp_path)
        assert tasks[0]["source"] == "AIVA"

    @pytest.mark.asyncio
    async def test_source_is_aiva_with_task_entity(self, tmp_path):
        worker = _make_worker(tmp_path)
        intent = _make_intent(task_entity="process lead list")
        await worker.execute(intent)
        tasks = _load_tasks(tmp_path)
        assert tasks[0]["source"] == "AIVA"

    @pytest.mark.asyncio
    async def test_all_tasks_have_aiva_source_after_multiple_dispatches(self, tmp_path):
        worker = _make_worker(tmp_path)
        for utterance in ["check voicemail", "send email", "update CRM"]:
            intent = _make_intent(utterance=utterance)
            await worker.execute(intent)
        tasks = _load_tasks(tmp_path)
        assert all(t["source"] == "AIVA" for t in tasks)


# ---------------------------------------------------------------------------
# BB3: Return dict has task_id as UUID string
# ---------------------------------------------------------------------------


class TestBB3_ReturnDictHasTaskId:
    """BB3 — execute() returns {"task_id": <valid UUID str>, "status": "queued"}."""

    @pytest.mark.asyncio
    async def test_execute_returns_dict(self, tmp_path):
        worker = _make_worker(tmp_path)
        intent = _make_intent()
        result = await worker.execute(intent)
        assert isinstance(result, dict)

    @pytest.mark.asyncio
    async def test_return_dict_has_task_id_key(self, tmp_path):
        worker = _make_worker(tmp_path)
        intent = _make_intent()
        result = await worker.execute(intent)
        assert "task_id" in result

    @pytest.mark.asyncio
    async def test_task_id_is_valid_uuid_string(self, tmp_path):
        worker = _make_worker(tmp_path)
        intent = _make_intent()
        result = await worker.execute(intent)
        # Must be parseable as a UUID — raises ValueError if not
        parsed = UUID(result["task_id"])
        assert str(parsed) == result["task_id"]

    @pytest.mark.asyncio
    async def test_return_status_is_queued(self, tmp_path):
        worker = _make_worker(tmp_path)
        intent = _make_intent()
        result = await worker.execute(intent)
        assert result["status"] == "queued"

    @pytest.mark.asyncio
    async def test_task_id_in_return_matches_task_id_in_file(self, tmp_path):
        """The returned task_id must equal the id stored in the JSON file."""
        worker = _make_worker(tmp_path)
        intent = _make_intent()
        result = await worker.execute(intent)
        tasks = _load_tasks(tmp_path)
        assert tasks[0]["id"] == result["task_id"]

    @pytest.mark.asyncio
    async def test_different_calls_return_different_task_ids(self, tmp_path):
        """Each execute() call must produce a unique UUID."""
        worker = _make_worker(tmp_path)
        ids = set()
        for _ in range(5):
            intent = _make_intent()
            result = await worker.execute(intent)
            ids.add(result["task_id"])
        assert len(ids) == 5


# ---------------------------------------------------------------------------
# WB1: Append to existing JSON array (not overwrite)
# ---------------------------------------------------------------------------


class TestWB1_AppendNotOverwrite:
    """WB1 — execute() appends to existing JSON; does not overwrite."""

    @pytest.mark.asyncio
    async def test_two_calls_produce_two_tasks(self, tmp_path):
        worker = _make_worker(tmp_path)
        for _ in range(2):
            await worker.execute(_make_intent())
        tasks = _load_tasks(tmp_path)
        assert len(tasks) == 2

    @pytest.mark.asyncio
    async def test_five_calls_produce_five_tasks(self, tmp_path):
        worker = _make_worker(tmp_path)
        for _ in range(5):
            await worker.execute(_make_intent())
        tasks = _load_tasks(tmp_path)
        assert len(tasks) == 5

    @pytest.mark.asyncio
    async def test_existing_task_is_preserved(self, tmp_path):
        """Pre-existing tasks in the JSON must not be lost on append."""
        tasks_file = tmp_path / "tasks.json"
        existing = [{"id": "pre-existing-001", "source": "manual", "status": "pending"}]
        tasks_file.write_text(json.dumps(existing, indent=2))

        worker = _make_worker(tmp_path)
        await worker.execute(_make_intent())

        tasks = _load_tasks(tmp_path)
        assert len(tasks) == 2
        ids = [t["id"] for t in tasks]
        assert "pre-existing-001" in ids

    @pytest.mark.asyncio
    async def test_missing_file_starts_fresh(self, tmp_path):
        """If no task board file exists, the worker creates one with a single entry."""
        worker = _make_worker(tmp_path)
        assert not (tmp_path / "tasks.json").exists()
        await worker.execute(_make_intent())
        tasks = _load_tasks(tmp_path)
        assert len(tasks) == 1

    @pytest.mark.asyncio
    async def test_task_board_is_valid_json(self, tmp_path):
        """After writing, the file must be parseable as JSON."""
        worker = _make_worker(tmp_path)
        await worker.execute(_make_intent())
        content = (tmp_path / "tasks.json").read_text()
        parsed = json.loads(content)
        assert isinstance(parsed, list)

    @pytest.mark.asyncio
    async def test_corrupt_json_file_starts_fresh(self, tmp_path):
        """A corrupt task board file must not cause execute() to crash."""
        tasks_file = tmp_path / "tasks.json"
        tasks_file.write_text("NOT VALID JSON AT ALL {{{}}")
        worker = _make_worker(tmp_path)
        result = await worker.execute(_make_intent())
        # Should succeed and write a fresh file
        assert result["status"] == "queued"
        tasks = _load_tasks(tmp_path)
        assert len(tasks) == 1


# ---------------------------------------------------------------------------
# WB2: Task description cleaned (not raw utterance text with filler words)
# ---------------------------------------------------------------------------


class TestWB2_TaskDescriptionCleaned:
    """WB2 — _build_task_description strips filler words from utterances."""

    def test_task_entity_takes_priority(self, tmp_path):
        """If extracted_entities contains 'task', use it verbatim."""
        worker = _make_worker(tmp_path)
        intent = _make_intent(
            utterance="um like just check the voicemail",
            task_entity="check voicemail messages",
        )
        description = worker._build_task_description(intent)
        assert description == "check voicemail messages"

    def test_filler_words_stripped_from_utterance(self, tmp_path):
        """Common filler words must be removed from the description."""
        worker = _make_worker(tmp_path)
        intent = _make_intent(utterance="um like just send the weekly report ok")
        description = worker._build_task_description(intent)
        # Filler words should be removed
        filler_present = any(
            word in description.lower().split()
            for word in ("um", "like", "just", "ok")
        )
        assert not filler_present

    def test_clean_utterance_retained(self, tmp_path):
        """Clean utterances (no filler words) should pass through intact."""
        worker = _make_worker(tmp_path)
        intent = _make_intent(utterance="send the weekly report to the team")
        description = worker._build_task_description(intent)
        assert "send" in description
        assert "weekly report" in description

    def test_empty_utterance_and_no_entity_returns_fallback(self, tmp_path):
        """No utterance and no task entity should return the safe fallback."""
        worker = _make_worker(tmp_path)
        intent = MagicMock()
        intent.utterance = ""
        intent.extracted_entities = {}
        description = worker._build_task_description(intent)
        assert description  # non-empty
        assert description == "AIVA task dispatch"

    def test_none_utterance_returns_fallback(self, tmp_path):
        """utterance=None should be handled gracefully."""
        worker = _make_worker(tmp_path)
        intent = MagicMock()
        intent.utterance = None
        intent.extracted_entities = {}
        description = worker._build_task_description(intent)
        assert description == "AIVA task dispatch"

    @pytest.mark.asyncio
    async def test_description_stored_in_file(self, tmp_path):
        """The cleaned description must be the one persisted to the task board."""
        worker = _make_worker(tmp_path)
        intent = _make_intent(task_entity="process the lead batch file")
        await worker.execute(intent)
        tasks = _load_tasks(tmp_path)
        assert tasks[0]["description"] == "process the lead batch file"


# ---------------------------------------------------------------------------
# WB3: UUID generated with uuid4()
# ---------------------------------------------------------------------------


class TestWB3_UUIDGeneratedWithUuid4:
    """WB3 — task IDs are UUID4 values (random, version 4)."""

    @pytest.mark.asyncio
    async def test_task_id_is_version_4_uuid(self, tmp_path):
        """The UUID stored in the task board must be version 4."""
        worker = _make_worker(tmp_path)
        intent = _make_intent()
        result = await worker.execute(intent)
        parsed = UUID(result["task_id"])
        assert parsed.version == 4

    @pytest.mark.asyncio
    async def test_consecutive_task_ids_are_unique(self, tmp_path):
        """Each UUID must be unique across calls."""
        worker = _make_worker(tmp_path)
        ids = []
        for _ in range(10):
            result = await worker.execute(_make_intent())
            ids.append(result["task_id"])
        assert len(set(ids)) == 10, "Duplicate UUIDs detected"

    @pytest.mark.asyncio
    async def test_task_id_in_file_matches_return_value(self, tmp_path):
        """UUID stored to disk must equal the UUID returned from execute()."""
        worker = _make_worker(tmp_path)
        intent = _make_intent()
        result = await worker.execute(intent)
        tasks = _load_tasks(tmp_path)
        assert tasks[0]["id"] == result["task_id"]

    @pytest.mark.asyncio
    async def test_task_id_is_string_not_uuid_object(self, tmp_path):
        """task_id in return dict must be a str, not a uuid.UUID object."""
        worker = _make_worker(tmp_path)
        intent = _make_intent()
        result = await worker.execute(intent)
        assert isinstance(result["task_id"], str)
