"""Tests for ArchitectureTracker."""

import asyncio
import json
import os
import tempfile
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from app_factory.core.architecture_tracker import ArchitectureTracker


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest.fixture
def tmp_data_dir(tmp_path):
    """Temporary data directory."""
    return str(tmp_path / "data")


@pytest.fixture
def tracker(tmp_data_dir):
    """ArchitectureTracker with no Claude SDK client."""
    return ArchitectureTracker(data_dir=tmp_data_dir)


@pytest.fixture
def tracker_with_client(tmp_data_dir):
    """ArchitectureTracker with a mocked Claude SDK client."""
    mock_client = AsyncMock()
    t = ArchitectureTracker(data_dir=tmp_data_dir)
    t._client = mock_client
    return t, mock_client


# ---------------------------------------------------------------------------
# Initialization
# ---------------------------------------------------------------------------

class TestInitialization:
    def test_creates_default_architecture(self, tracker):
        arch = tracker._architecture
        assert "modules" in arch
        assert "utilities" in arch
        assert "design_patterns" in arch
        assert "naming_conventions" in arch
        assert "tech_stack" in arch
        assert arch["version"] == 1
        assert "last_updated" in arch

    def test_creates_data_directory(self, tmp_data_dir):
        tracker = ArchitectureTracker(data_dir=tmp_data_dir)
        assert os.path.isdir(tmp_data_dir)

    def test_default_architecture_has_empty_lists(self, tracker):
        assert tracker._architecture["modules"] == []
        assert tracker._architecture["utilities"] == []
        assert tracker._architecture["design_patterns"] == []

    def test_default_naming_conventions(self, tracker):
        conventions = tracker._architecture["naming_conventions"]
        assert conventions["variables"] == "snake_case"
        assert conventions["classes"] == "PascalCase"


# ---------------------------------------------------------------------------
# Load / Save Persistence
# ---------------------------------------------------------------------------

class TestPersistence:
    def test_save_and_load(self, tmp_data_dir):
        tracker1 = ArchitectureTracker(data_dir=tmp_data_dir)
        tracker1.add_module("TestModule", "A test module", "test.py")

        tracker2 = ArchitectureTracker(data_dir=tmp_data_dir)
        assert len(tracker2._architecture["modules"]) == 1
        assert tracker2._architecture["modules"][0]["name"] == "TestModule"

    def test_save_updates_timestamp(self, tracker):
        old_ts = tracker._architecture.get("last_updated", "")
        tracker.save_architecture(tracker._architecture)
        new_ts = tracker._architecture["last_updated"]
        # Timestamp should be updated (or at least present)
        assert new_ts is not None
        assert len(new_ts) > 0

    def test_load_corrupt_file_returns_default(self, tmp_data_dir):
        os.makedirs(tmp_data_dir, exist_ok=True)
        corrupt_path = os.path.join(tmp_data_dir, "global_architecture.json")
        with open(corrupt_path, "w") as f:
            f.write("NOT VALID JSON {{{")

        tracker = ArchitectureTracker(data_dir=tmp_data_dir)
        assert tracker._architecture["version"] == 1
        assert tracker._architecture["modules"] == []


# ---------------------------------------------------------------------------
# add_module
# ---------------------------------------------------------------------------

class TestAddModule:
    def test_add_module(self, tracker):
        tracker.add_module("MyModule", "Does something", "src/my_module.py")
        assert len(tracker._architecture["modules"]) == 1
        mod = tracker._architecture["modules"][0]
        assert mod["name"] == "MyModule"
        assert mod["purpose"] == "Does something"
        assert mod["file_path"] == "src/my_module.py"

    def test_add_module_persists(self, tmp_data_dir):
        t1 = ArchitectureTracker(data_dir=tmp_data_dir)
        t1.add_module("Persisted", "persists", "p.py")

        t2 = ArchitectureTracker(data_dir=tmp_data_dir)
        assert any(m["name"] == "Persisted" for m in t2._architecture["modules"])


# ---------------------------------------------------------------------------
# add_utility
# ---------------------------------------------------------------------------

class TestAddUtility:
    def test_add_utility(self, tracker):
        tracker.add_utility("helper_func", "Helps with things", "utils.py")
        assert len(tracker._architecture["utilities"]) == 1
        util = tracker._architecture["utilities"][0]
        assert util["name"] == "helper_func"
        assert util["description"] == "Helps with things"
        assert util["file_path"] == "utils.py"

    def test_add_utility_persists(self, tmp_data_dir):
        t1 = ArchitectureTracker(data_dir=tmp_data_dir)
        t1.add_utility("persisted_func", "persists", "p.py")

        t2 = ArchitectureTracker(data_dir=tmp_data_dir)
        assert any(u["name"] == "persisted_func" for u in t2._architecture["utilities"])


# ---------------------------------------------------------------------------
# get_architecture_summary
# ---------------------------------------------------------------------------

class TestGetArchitectureSummary:
    def test_returns_formatted_string(self, tracker):
        tracker.add_module("GraphEngine", "Runs the graph", "graph.py")
        tracker.add_utility("parse_json", "Parses JSON input", "utils.py")
        summary = tracker.get_architecture_summary()
        assert isinstance(summary, str)
        assert "Project Architecture Summary" in summary
        assert "GraphEngine" in summary
        assert "parse_json" in summary

    def test_includes_tech_stack(self, tracker):
        summary = tracker.get_architecture_summary()
        assert "Tech Stack" in summary
        assert "Python" in summary

    def test_includes_naming_conventions(self, tracker):
        summary = tracker.get_architecture_summary()
        assert "Naming Conventions" in summary
        assert "snake_case" in summary

    def test_respects_max_tokens_limit(self, tracker):
        # Add many modules to produce a large summary
        for i in range(200):
            tracker._architecture["modules"].append({
                "name": f"Module_{i}",
                "purpose": f"Purpose of module {i} with extra text for padding",
                "file_path": f"src/module_{i}.py",
            })
        # Very small token limit
        summary = tracker.get_architecture_summary(max_tokens=50)
        # 50 tokens * 4 chars = 200 chars max
        assert len(summary) <= 200

    def test_empty_architecture_still_returns_summary(self, tracker):
        summary = tracker.get_architecture_summary()
        assert "Project Architecture Summary" in summary


# ---------------------------------------------------------------------------
# update_architecture (async)
# ---------------------------------------------------------------------------

class TestUpdateArchitecture:
    def test_basic_extraction_no_client(self, tracker, tmp_path):
        # Create a sample Python file
        py_file = tmp_path / "sample.py"
        py_file.write_text(
            '"""Sample module."""\n\n'
            'class SampleClass:\n'
            '    """A sample class for testing."""\n'
            '    pass\n\n'
            'def sample_function():\n'
            '    """A sample utility function."""\n'
            '    pass\n\n'
            'def _private_function():\n'
            '    """Should be skipped."""\n'
            '    pass\n'
        )

        task = {"title": "Test task", "description": "Testing extraction"}
        asyncio.run(tracker.update_architecture(task, [str(py_file)]))

        modules = tracker._architecture["modules"]
        utilities = tracker._architecture["utilities"]
        assert any(m["name"] == "SampleClass" for m in modules)
        assert any(u["name"] == "sample_function" for u in utilities)
        # Private functions should be skipped
        assert not any(u["name"] == "_private_function" for u in utilities)

    def test_skips_non_python_files(self, tracker, tmp_path):
        txt_file = tmp_path / "readme.txt"
        txt_file.write_text("Not a Python file")

        task = {"title": "Test task"}
        asyncio.run(tracker.update_architecture(task, [str(txt_file)]))
        assert tracker._architecture["modules"] == []
        assert tracker._architecture["utilities"] == []

    def test_skips_nonexistent_files(self, tracker):
        task = {"title": "Test task"}
        asyncio.run(tracker.update_architecture(task, ["/nonexistent/file.py"]))
        assert tracker._architecture["modules"] == []

    def test_ai_extraction_with_mock_client(self, tracker_with_client, tmp_path):
        tracker, mock_client = tracker_with_client

        py_file = tmp_path / "ai_sample.py"
        py_file.write_text(
            'class AIExtracted:\n'
            '    """Extracted by AI."""\n'
            '    pass\n'
        )

        mock_response = MagicMock()
        mock_response.text = json.dumps({
            "classes": [{"name": "AIExtracted", "purpose": "AI detected class"}],
            "functions": [{"name": "ai_helper", "description": "AI detected function"}],
        })
        mock_client.complete.return_value = mock_response

        task = {"title": "AI test task"}
        asyncio.run(tracker.update_architecture(task, [str(py_file)]))

        modules = tracker._architecture["modules"]
        utilities = tracker._architecture["utilities"]
        assert any(m["name"] == "AIExtracted" for m in modules)
        assert any(u["name"] == "ai_helper" for u in utilities)

    def test_ai_extraction_fallback_on_failure(self, tracker_with_client, tmp_path):
        tracker, mock_client = tracker_with_client

        py_file = tmp_path / "fallback_sample.py"
        py_file.write_text(
            'class FallbackClass:\n'
            '    """Falls back to basic extraction."""\n'
            '    pass\n'
        )

        mock_client.complete.side_effect = Exception("API error")

        task = {"title": "Fallback test"}
        asyncio.run(tracker.update_architecture(task, [str(py_file)]))

        # Should fall back to basic extraction
        modules = tracker._architecture["modules"]
        assert any(m["name"] == "FallbackClass" for m in modules)

    def test_no_duplicate_modules(self, tracker, tmp_path):
        py_file = tmp_path / "dup.py"
        py_file.write_text('class DupClass:\n    pass\n')

        task = {"title": "Dup test"}
        asyncio.run(tracker.update_architecture(task, [str(py_file)]))
        asyncio.run(tracker.update_architecture(task, [str(py_file)]))

        names = [m["name"] for m in tracker._architecture["modules"]]
        assert names.count("DupClass") == 1
