"""
ShelfAI test suite.

Tests core functionality: shelf operations, index parsing,
transcript parsing, session management, and CLI commands.
"""

import json


class MockProvider:
    """Mock LLM provider for testing."""

    def __init__(self, responses=None):
        self.responses = responses or []
        self._call_index = 0
        self.calls = []

    def complete(self, system, user, max_tokens=4096):
        self.calls.append({"system": system, "user": user})
        if self._call_index < len(self.responses):
            response = self.responses[self._call_index]
            self._call_index += 1
            return response
        return json.dumps({
            "user_preferences": [],
            "agent_lessons": [],
            "task_patterns": [],
            "knowledge_corrections": [],
            "new_context": [],
            "abstract_updates": [],
        })


class FailingProvider:
    """Mock provider that raises exceptions."""

    def complete(self, system, user, max_tokens=4096):
        raise ConnectionError("API is down")


class GarbageProvider:
    """Mock provider that returns non-JSON garbage."""

    def __init__(self):
        self.calls = []

    def complete(self, system, user, max_tokens=4096):
        self.calls.append(True)
        return "Sorry, I can't help with that. Here's some thoughts:\n\nThe data looks..."


class TestSessionManager:
    """Tests for the session management agent."""

    def test_empty_transcript(self, shelf_dir):
        from shelfai.core.shelf import Shelf
        from shelfai.agents.session import SessionManager

        shelf = Shelf(str(shelf_dir))
        provider = MockProvider()
        manager = SessionManager(shelf, provider)

        report = manager.process_text("")
        assert report.extraction.is_empty

    def test_no_learnings(self, shelf_dir):
        from shelfai.core.shelf import Shelf
        from shelfai.agents.session import SessionManager

        shelf = Shelf(str(shelf_dir))

        empty_extraction = json.dumps({
            "user_preferences": [],
            "agent_lessons": [],
            "task_patterns": [],
            "knowledge_corrections": [],
            "new_context": [],
            "abstract_updates": [],
        })

        provider = MockProvider(responses=[empty_extraction])
        manager = SessionManager(shelf, provider)

        report = manager.process_text("## User\nHello\n\n## Assistant\nHi!")
        assert report.extraction.is_empty

    def test_extract_preferences(self, shelf_dir):
        from shelfai.core.shelf import Shelf
        from shelfai.agents.session import SessionManager

        shelf = Shelf(str(shelf_dir))

        extraction = json.dumps({
            "user_preferences": [
                {
                    "content": "Prefers concise responses",
                    "confidence": "high",
                    "evidence": "User said keep it short",
                }
            ],
            "agent_lessons": [],
            "task_patterns": [],
            "knowledge_corrections": [],
            "new_context": [],
            "abstract_updates": [],
        })

        # Dedup returns same thing (nothing to remove)
        dedup_response = json.dumps({
            "user_preferences": [
                {
                    "content": "Prefers concise responses",
                    "confidence": "high",
                    "evidence": "User said keep it short",
                }
            ],
            "agent_lessons": [],
            "task_patterns": [],
        })

        provider = MockProvider(responses=[extraction, dedup_response])
        manager = SessionManager(shelf, provider)

        report = manager.process_text(
            "## User\nKeep it short please.\n\n## Assistant\nSure!",
        )

        assert len(report.extraction.user_preferences) == 1
        assert any(
            c.get("type") == "user_preference" for c in report.applied_changes
        )

        # Verify it was written to the file
        content = shelf.read_file("memory/user/preferences.md")
        assert "concise" in content.lower()

    def test_dry_run(self, shelf_dir):
        from shelfai.core.shelf import Shelf
        from shelfai.agents.session import SessionManager

        shelf = Shelf(str(shelf_dir))

        extraction = json.dumps({
            "user_preferences": [
                {
                    "content": "Prefers Python",
                    "confidence": "high",
                    "evidence": "Asked for Python examples",
                }
            ],
            "agent_lessons": [],
            "task_patterns": [],
            "knowledge_corrections": [],
            "new_context": [],
            "abstract_updates": [],
        })

        dedup_response = json.dumps({
            "user_preferences": [
                {"content": "Prefers Python", "confidence": "high", "evidence": "..."}
            ],
            "agent_lessons": [],
            "task_patterns": [],
        })

        provider = MockProvider(responses=[extraction, dedup_response])
        manager = SessionManager(shelf, provider)

        report = manager.process_text(
            "## User\nShow me Python examples.\n\n## Assistant\nHere you go.",
            dry_run=True,
        )

        assert report.dry_run
        assert len(report.applied_changes) > 0

        # Verify nothing was actually written
        content = shelf.read_file("memory/user/preferences.md")
        assert "Python" not in content

    def test_long_transcript_truncated(self, shelf_dir):
        """Transcripts with many messages should be truncated at message boundaries."""
        from shelfai.core.shelf import Shelf
        from shelfai.agents.session import SessionManager

        shelf = Shelf(str(shelf_dir))

        empty_extraction = json.dumps({
            "user_preferences": [],
            "agent_lessons": [],
            "task_patterns": [],
            "knowledge_corrections": [],
            "new_context": [],
            "abstract_updates": [],
        })

        provider = MockProvider(responses=[empty_extraction])
        manager = SessionManager(shelf, provider)

        # Create a transcript with many long messages to exceed the 60k char limit
        messages = []
        for i in range(100):
            messages.append(f"## User\n{'x' * 800} message {i}\n")
            messages.append(f"## Assistant\n{'y' * 800} response {i}\n")
        huge_transcript = "\n".join(messages)

        report = manager.process_text(huge_transcript)

        assert report is not None
        assert len(provider.calls) >= 1
        # Message-based truncation uses "omitted" marker
        call_text = provider.calls[0]["user"]
        assert "omitted" in call_text.lower()

    def test_abstract_update_with_missing_fields(self, shelf_dir):
        """Abstract updates with missing fields should skip gracefully, not KeyError."""
        from shelfai.core.shelf import Shelf
        from shelfai.agents.session import SessionManager

        shelf = Shelf(str(shelf_dir))

        extraction = json.dumps({
            "user_preferences": [],
            "agent_lessons": [],
            "task_patterns": [],
            "knowledge_corrections": [],
            "new_context": [],
            "abstract_updates": [
                {"file": "skills/code_review.md"}
                # Missing "suggested_abstract" — should be handled gracefully
            ],
        })

        provider = MockProvider(responses=[extraction])
        manager = SessionManager(shelf, provider)

        report = manager.process_text("## User\nHi\n\n## Assistant\nHello")
        # Should not crash — the malformed update should be caught by schema validation
        assert report is not None


# ──────────────────────────────────────────────
# Test: File Operations
# ──────────────────────────────────────────────


class TestJSONParsing:
    """Tests for robust LLM JSON response parsing."""

    def test_clean_json(self):
        from shelfai.agents.session import _parse_json_response

        result = _parse_json_response('{"key": "value"}')
        assert result == {"key": "value"}

    def test_json_with_code_fence(self):
        from shelfai.agents.session import _parse_json_response

        result = _parse_json_response('```json\n{"key": "value"}\n```')
        assert result == {"key": "value"}

    def test_json_with_preamble(self):
        from shelfai.agents.session import _parse_json_response

        result = _parse_json_response('Here is the JSON:\n\n{"key": "value"}')
        assert result == {"key": "value"}

    def test_json_with_trailing_text(self):
        from shelfai.agents.session import _parse_json_response

        result = _parse_json_response('{"key": "value"}\n\nHope that helps!')
        assert result == {"key": "value"}

    def test_garbage_returns_none(self):
        from shelfai.agents.session import _parse_json_response

        result = _parse_json_response("This is not JSON at all")
        assert result is None

    def test_empty_returns_none(self):
        from shelfai.agents.session import _parse_json_response

        result = _parse_json_response("")
        assert result is None

    def test_json_between_conversational_text(self):
        """The greedy regex bug: text before AND after the JSON object."""
        from shelfai.agents.session import _parse_json_response

        text = 'Here\'s the result: {"key": "value"} and some trailing text {"garbage": true}'
        result = _parse_json_response(text)
        assert result == {"key": "value"}

    def test_json_with_sentence_after(self):
        """LLM adds a sentence after the JSON."""
        from shelfai.agents.session import _parse_json_response

        text = 'I\'ll structure this as JSON: {"key": "value"}. Let me also note that this is great.'
        result = _parse_json_response(text)
        assert result == {"key": "value"}

    def test_nested_json_braces(self):
        """JSON with nested objects — brace balancing must handle depth."""
        from shelfai.agents.session import _parse_json_response

        text = 'Result: {"outer": {"inner": [1, 2, 3]}, "other": "val"}'
        result = _parse_json_response(text)
        assert result == {"outer": {"inner": [1, 2, 3]}, "other": "val"}

    def test_json_with_escaped_braces_in_strings(self):
        """JSON containing brace characters inside string values."""
        from shelfai.agents.session import _parse_json_response

        text = '{"content": "Use when user says {help}"}'
        result = _parse_json_response(text)
        assert result == {"content": "Use when user says {help}"}

    def test_extract_json_block_directly(self):
        """Test the brace-balancing extractor directly."""
        from shelfai.agents.session import _extract_json_block

        assert _extract_json_block('no json here') is None
        assert _extract_json_block('{"a": 1}') == '{"a": 1}'
        assert _extract_json_block('prefix {"a": 1} suffix') == '{"a": 1}'
        assert _extract_json_block('{"a": {"b": 2}}') == '{"a": {"b": 2}}'

    def test_extract_json_array(self):
        """Brace balancer should also find top-level JSON arrays."""
        from shelfai.agents.session import _extract_json_block

        assert _extract_json_block('Here: [1, 2, 3] done') == '[1, 2, 3]'
        assert _extract_json_block('[{"a": 1}, {"b": 2}]') == '[{"a": 1}, {"b": 2}]'
        assert _extract_json_block('prefix [{"nested": [1]}] suffix') == '[{"nested": [1]}]'

    def test_extract_json_object_before_array(self):
        """When both { and [ exist, pick whichever comes first."""
        from shelfai.agents.session import _extract_json_block

        # Object first
        assert _extract_json_block('{"a": 1} [1, 2]') == '{"a": 1}'
        # Array first
        assert _extract_json_block('[1, 2] {"a": 1}') == '[1, 2]'

    def test_escape_only_inside_strings(self):
        """Backslash outside strings should not trigger escape behavior."""
        from shelfai.agents.session import _extract_json_block

        # Backslash in a string value — should be handled as escape
        result = _extract_json_block(r'{"path": "C:\\Users\\test"}')
        assert result is not None
        # The block should be extractable and parseable
        import json
        parsed = json.loads(result)
        assert parsed["path"] == "C:\\Users\\test"

    def test_parse_json_array_response(self):
        """Full parser should handle array responses too."""
        from shelfai.agents.session import _parse_json_response

        result = _parse_json_response('Here are the results: [{"a": 1}, {"b": 2}]')
        assert result == [{"a": 1}, {"b": 2}]


# ──────────────────────────────────────────────
# Test: Safety — Schema Validation
# ──────────────────────────────────────────────


class TestSessionManagerFailures:
    """Tests for session manager failure modes."""

    def test_llm_failure_returns_empty(self, shelf_dir):
        from shelfai.core.shelf import Shelf
        from shelfai.agents.session import SessionManager

        shelf = Shelf(str(shelf_dir))
        provider = FailingProvider()
        manager = SessionManager(shelf, provider)

        report = manager.process_text("## User\nHello\n\n## Assistant\nHi!")
        # Should not crash, should return empty extraction
        assert report.extraction.is_empty

    def test_garbage_response_returns_empty(self, shelf_dir):
        from shelfai.core.shelf import Shelf
        from shelfai.agents.session import SessionManager

        shelf = Shelf(str(shelf_dir))
        provider = GarbageProvider()
        manager = SessionManager(shelf, provider)

        report = manager.process_text("## User\nHello\n\n## Assistant\nHi!")
        assert report.extraction.is_empty


# ──────────────────────────────────────────────
# Test: Additional Edge Cases
# ──────────────────────────────────────────────


class FailOnSecondCallProvider:
    """Mock provider that succeeds on first call, fails on second (dedup)."""

    def __init__(self):
        self._calls = 0

    def complete(self, system, user, max_tokens=4096):
        self._calls += 1
        if self._calls == 1:
            return json.dumps({
                "user_preferences": [
                    {"content": "test pref", "confidence": "high", "evidence": "e"}
                ],
                "agent_lessons": [],
                "task_patterns": [],
                "knowledge_corrections": [],
                "new_context": [],
                "abstract_updates": [],
            })
        raise ConnectionError("Dedup call failed")


class TestSessionPolicyEnforcement:
    def test_require_high_confidence_filters_medium(self, shelf_dir):
        from shelfai.core.shelf import Shelf
        from shelfai.agents.session import SessionManager

        config_path = shelf_dir / "shelf.config.yaml"
        config_path.write_text(
            "version: '1.0'\n"
            "project:\n"
            "  name: test-project\n"
            "session:\n"
            "  provider: anthropic\n"
            "  model: claude-sonnet-4-20250514\n"
            "  require_high_confidence: true\n",
            encoding="utf-8",
        )

        shelf = Shelf(str(shelf_dir))
        extraction = json.dumps({
            "user_preferences": [
                {
                    "content": "Prefers concise responses",
                    "confidence": "medium",
                    "evidence": "User said maybe keep it short",
                }
            ],
            "agent_lessons": [],
            "task_patterns": [],
            "knowledge_corrections": [],
            "new_context": [],
            "abstract_updates": [],
        })
        provider = MockProvider(responses=[extraction])
        manager = SessionManager(shelf, provider)

        report = manager.process_text("## User\nMaybe keep it short.\n")
        assert report.extraction.is_empty

    def test_disable_new_context_blocks_creation(self, shelf_dir):
        from shelfai.core.shelf import Shelf
        from shelfai.agents.session import SessionManager

        config_path = shelf_dir / "shelf.config.yaml"
        config_path.write_text(
            "version: '1.0'\n"
            "project:\n"
            "  name: test-project\n"
            "session:\n"
            "  provider: anthropic\n"
            "  model: claude-sonnet-4-20250514\n"
            "  extract:\n"
            "    new_context: false\n",
            encoding="utf-8",
        )

        shelf = Shelf(str(shelf_dir))
        extraction = json.dumps({
            "user_preferences": [],
            "agent_lessons": [],
            "task_patterns": [],
            "knowledge_corrections": [],
            "new_context": [
                {
                    "suggested_path": "memory/agent/new_note.md",
                    "suggested_abstract": "Use for testing.",
                    "content_summary": "A test note.",
                    "evidence": "Mentioned in the session",
                }
            ],
            "abstract_updates": [],
        })
        provider = MockProvider(responses=[extraction])
        manager = SessionManager(shelf, provider)

        report = manager.process_text("## User\nStore this.\n")
        assert report.extraction.new_context == []
        assert not (shelf_dir / "memory" / "agent" / "new_note.md").exists()

    def test_new_context_respects_allowlist_and_markdown_extension(self, shelf_dir):
        from shelfai.core.shelf import Shelf
        from shelfai.agents.session import SessionManager, Extraction

        shelf = Shelf(str(shelf_dir))
        manager = SessionManager(shelf, MockProvider())
        extraction = Extraction(
            new_context=[
                {
                    "suggested_path": "knowledge/bad.txt",
                    "suggested_abstract": "Should fail.",
                    "content_summary": "Not allowed.",
                }
            ]
        )

        report = manager._apply_changes(extraction, session_id="test-session")
        assert report.errors
        assert "allowed" in report.errors[0]["error"] or ".md" in report.errors[0]["error"]

    def test_stage_mode_allows_non_memory_categories_when_allowlisted(self, shelf_dir):
        from shelfai.core.shelf import Shelf
        from shelfai.agents.session import SessionManager

        (shelf_dir / "shelf.config.yaml").write_text(
            "version: '1.0'\n"
            "project:\n"
            "  name: test-project\n"
            "session:\n"
            "  provider: anthropic\n"
            "  model: claude-sonnet-4-20250514\n"
            "  new_context_mode: stage\n"
            "  new_context_allowed_categories:\n"
            "    - memory\n"
            "    - knowledge\n",
            encoding="utf-8",
        )

        shelf = Shelf(str(shelf_dir))
        extraction = json.dumps({
            "user_preferences": [],
            "agent_lessons": [],
            "task_patterns": [],
            "knowledge_corrections": [],
            "new_context": [
                {
                    "suggested_path": "knowledge/team-playbook.md",
                    "suggested_abstract": "Use for team onboarding, roles, and operating norms when answering organization workflow questions.",
                    "content_summary": "Shared notes on team onboarding and operating norms.",
                    "evidence": "Mentioned in session",
                }
            ],
            "abstract_updates": [],
        })
        provider = MockProvider(responses=[extraction])
        manager = SessionManager(shelf, provider)

        report = manager.process_text("## User\nStore this context.\n")
        staged = list((shelf_dir / "generated" / "review" / "new_context").glob("*.md"))
        assert staged
        raw = staged[0].read_text(encoding="utf-8")
        assert "proposed_path: knowledge/team-playbook.md" in raw
        assert not (shelf_dir / "knowledge" / "team-playbook.md").exists()
        assert any(c.get("type") == "staged_new_file" for c in report.applied_changes)

    def test_apply_mode_rejects_non_memory_categories_even_if_allowlisted(self, shelf_dir):
        from shelfai.core.shelf import Shelf
        from shelfai.agents.session import SessionManager

        (shelf_dir / "shelf.config.yaml").write_text(
            "version: '1.0'\n"
            "project:\n"
            "  name: test-project\n"
            "session:\n"
            "  provider: anthropic\n"
            "  model: claude-sonnet-4-20250514\n"
            "  new_context_mode: apply\n"
            "  new_context_allowed_categories:\n"
            "    - memory\n"
            "    - skills\n",
            encoding="utf-8",
        )

        shelf = Shelf(str(shelf_dir))
        extraction = json.dumps({
            "user_preferences": [],
            "agent_lessons": [],
            "task_patterns": [],
            "knowledge_corrections": [],
            "new_context": [
                {
                    "suggested_path": "skills/outreach-email.md",
                    "suggested_abstract": "Use for writing outreach emails and follow-up messaging in a repeatable workflow.",
                    "content_summary": "Template and process for outreach emails.",
                    "evidence": "Mentioned in session",
                }
            ],
            "abstract_updates": [],
        })
        provider = MockProvider(responses=[extraction])
        manager = SessionManager(shelf, provider)

        report = manager.process_text("## User\nStore this context.\n")
        assert report.errors
        assert "apply mode may only auto-create files in memory/" in report.errors[0]["error"].lower()
        assert not (shelf_dir / "skills" / "outreach-email.md").exists()


class TestSessionV012Features:
    def test_new_context_stages_for_review_by_default(self, shelf_dir):
        from shelfai.core.shelf import Shelf
        from shelfai.agents.session import SessionManager

        shelf = Shelf(str(shelf_dir))
        extraction = json.dumps({
            "user_preferences": [],
            "agent_lessons": [],
            "task_patterns": [],
            "knowledge_corrections": [],
            "new_context": [
                {
                    "suggested_path": "memory/agent/team_playbook.md",
                    "suggested_abstract": "Use when handling team playbook context and onboarding workflow questions.",
                    "content_summary": "Shared notes on team onboarding and operating norms.",
                    "evidence": "Mentioned in session",
                }
            ],
            "abstract_updates": [],
        })
        provider = MockProvider(responses=[extraction])
        manager = SessionManager(shelf, provider)

        report = manager.process_text("## User\nStore this context.\n")

        staged = list((shelf_dir / "generated" / "review" / "new_context").glob("*.md"))
        assert staged
        assert not (shelf_dir / "memory" / "agent" / "team_playbook.md").exists()
        assert any(c.get("type") == "staged_new_file" for c in report.applied_changes)

    def test_new_context_apply_mode_creates_file(self, shelf_dir):
        from shelfai.core.shelf import Shelf
        from shelfai.agents.session import SessionManager

        (shelf_dir / "shelf.config.yaml").write_text(
            "version: '1.0'\n"
            "project:\n"
            "  name: test-project\n"
            "session:\n"
            "  provider: anthropic\n"
            "  model: claude-sonnet-4-20250514\n"
            "  new_context_mode: apply\n",
            encoding="utf-8",
        )

        shelf = Shelf(str(shelf_dir))
        extraction = json.dumps({
            "user_preferences": [],
            "agent_lessons": [],
            "task_patterns": [],
            "knowledge_corrections": [],
            "new_context": [
                {
                    "suggested_path": "memory/agent/team_playbook.md",
                    "suggested_abstract": "Use when handling team playbook context and onboarding workflow questions.",
                    "content_summary": "Shared notes on team onboarding and operating norms.",
                    "evidence": "Mentioned in session",
                }
            ],
            "abstract_updates": [],
        })
        provider = MockProvider(responses=[extraction])
        manager = SessionManager(shelf, provider)

        report = manager.process_text("## User\nStore this context.\n")
        assert (shelf_dir / "memory" / "agent" / "team_playbook.md").exists()
        assert any(c.get("type") == "new_file" for c in report.applied_changes)

    def test_generic_abstract_is_rejected(self, shelf_dir):
        from shelfai.core.shelf import Shelf
        from shelfai.agents.session import SessionManager, Extraction

        shelf = Shelf(str(shelf_dir))
        manager = SessionManager(shelf, MockProvider())
        extraction = Extraction(
            new_context=[
                {
                    "suggested_path": "memory/agent/bad_note.md",
                    "suggested_abstract": "General notes",
                    "content_summary": "Too vague.",
                }
            ]
        )
        report = manager._apply_changes(extraction, session_id="lint-test")
        assert report.errors
        assert "generic" in report.errors[0]["error"].lower() or "discriminative" in report.errors[0]["error"].lower()

    def test_generated_review_files_are_not_listed_as_content(self, shelf_dir):
        from shelfai.core.shelf import Shelf

        generated = shelf_dir / "generated" / "review" / "new_context"
        generated.mkdir(parents=True, exist_ok=True)
        (generated / "proposal.md").write_text("# generated artifact", encoding="utf-8")

        shelf = Shelf(str(shelf_dir))
        files = shelf.list_files()
        assert "generated/review/new_context/proposal.md" not in files
