from __future__ import annotations

import ast
import hashlib
import unittest
from pathlib import Path

import assistant_agent
from backend.features.review import agent as canonical_agent


APP_ROOT = Path(__file__).resolve().parents[1]
ORIGINAL_ROOT = APP_ROOT.parent

ALERT_SERVICE_METHODS = {
    "alert_center",
    "create_alert",
    "mark_alert_read",
    "mark_all_alerts_read",
    "delete_alert",
}

REVIEW_SERVICE_METHODS = {
    "trade_entries",
    "review_watchlist",
    "save_trade_entry",
    "delete_trade_entry",
    "assistant_messages",
    "clear_assistant_messages",
    "assistant_stream",
    "_assistant_context",
}

ALERT_REPOSITORY_METHODS = {
    "save_alert",
    "list_alerts",
    "count_unread_alerts",
    "mark_alert_read",
    "mark_all_alerts_read",
    "delete_alert",
}

REVIEW_REPOSITORY_METHODS = {
    "list_watchlist",
    "save_watchlist",
    "watchlist_price_history",
    "delete_watchlist",
    "list_notes",
    "save_note",
    "delete_note",
    "save_trade_entry",
    "list_trade_entries",
    "delete_trade_entry",
    "save_assistant_exchange",
    "list_assistant_messages",
    "delete_assistant_messages",
}

ALERT_HTTP_METHODS = {"save_alert"}
REVIEW_HTTP_METHODS = {
    "save_trade_entry",
    "stream_assistant_chat",
    "save_watchlist",
    "save_note",
}


def sha256(path: Path) -> str:
    return hashlib.sha256(path.read_bytes()).hexdigest()


def class_methods(path: Path, class_name: str) -> dict[str, str]:
    tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
    owner = next(
        node
        for node in tree.body
        if isinstance(node, ast.ClassDef) and node.name == class_name
    )
    return {
        node.name: ast.dump(node, include_attributes=False)
        for node in owner.body
        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
    }


def top_level_definition(path: Path, name: str) -> str:
    tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
    node = next(
        item
        for item in tree.body
        if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef))
        and item.name == name
    )
    return ast.dump(node, include_attributes=False)


class ReviewAlertsSliceSourceEquivalenceTests(unittest.TestCase):
    def assert_methods_equal(
        self,
        original_path: Path,
        original_class: str,
        migrated_path: Path,
        migrated_class: str,
        expected: set[str],
    ) -> None:
        original = class_methods(original_path, original_class)
        migrated = class_methods(migrated_path, migrated_class)
        self.assertEqual(set(migrated), expected)
        for name in sorted(expected):
            self.assertEqual(migrated[name], original[name], name)

    def test_review_assistant_agent_uses_shared_transport(self) -> None:
        source = (
            APP_ROOT / "backend" / "features" / "review" / "agent.py"
        ).read_text(encoding="utf-8")
        self.assertIn("llm_transport.stream_chat_completion", source)
        self.assertNotIn("urllib.request", source)

    def test_review_assistant_compatibility_module_is_canonical(self) -> None:
        self.assertIs(assistant_agent, canonical_agent)

    def test_alert_and_review_service_methods_are_exact_original_ast(self) -> None:
        self.assert_methods_equal(
            ORIGINAL_ROOT / "server.py",
            "DashboardService",
            APP_ROOT / "backend" / "features" / "alerts" / "facade.py",
            "AlertServiceMixin",
            ALERT_SERVICE_METHODS,
        )
        self.assert_methods_equal(
            ORIGINAL_ROOT / "server.py",
            "DashboardService",
            APP_ROOT / "backend" / "features" / "review" / "service.py",
            "ReviewServiceMixin",
            REVIEW_SERVICE_METHODS,
        )

    def test_alert_and_review_repository_methods_are_exact_original_ast(self) -> None:
        self.assert_methods_equal(
            ORIGINAL_ROOT / "database.py",
            "ReviewDatabase",
            APP_ROOT / "backend" / "features" / "alerts" / "repository.py",
            "AlertRepositoryMixin",
            ALERT_REPOSITORY_METHODS,
        )
        self.assert_methods_equal(
            ORIGINAL_ROOT / "database.py",
            "ReviewDatabase",
            APP_ROOT / "backend" / "features" / "review" / "repository.py",
            "ReviewRepositoryMixin",
            REVIEW_REPOSITORY_METHODS,
        )

    def test_existing_alert_and_trade_journal_services_preserve_original_classes(self) -> None:
        self.assertEqual(
            top_level_definition(
                ORIGINAL_ROOT / "backend" / "features" / "alerts" / "service.py",
                "AlertService",
            ),
            top_level_definition(
                APP_ROOT / "backend" / "features" / "alerts" / "service.py",
                "AlertService",
            ),
        )
        self.assertEqual(
            top_level_definition(
                ORIGINAL_ROOT / "backend" / "features" / "review" / "trade_journal.py",
                "TradeJournalService",
            ),
            top_level_definition(
                APP_ROOT / "backend" / "features" / "review" / "trade_journal.py",
                "TradeJournalService",
            ),
        )

    def test_original_classes_no_longer_duplicate_moved_methods(self) -> None:
        remaining_service = class_methods(
            APP_ROOT / "backend" / "application.py", "DashboardService"
        )
        remaining_database = class_methods(APP_ROOT / "database.py", "ReviewDatabase")
        remaining_http = class_methods(
            APP_ROOT / "backend" / "application.py", "RequestHandler"
        )
        self.assertTrue(ALERT_SERVICE_METHODS.isdisjoint(remaining_service))
        self.assertTrue(REVIEW_SERVICE_METHODS.isdisjoint(remaining_service))
        self.assertTrue(ALERT_REPOSITORY_METHODS.isdisjoint(remaining_database))
        self.assertTrue(REVIEW_REPOSITORY_METHODS.isdisjoint(remaining_database))
        self.assertTrue(ALERT_HTTP_METHODS.isdisjoint(remaining_http))
        self.assertTrue(REVIEW_HTTP_METHODS.isdisjoint(remaining_http))

    def test_http_mixins_preserve_all_endpoints_without_global_service(self) -> None:
        alerts = class_methods(
            APP_ROOT / "backend" / "features" / "alerts" / "http.py",
            "AlertHttpMixin",
        )
        review = class_methods(
            APP_ROOT / "backend" / "features" / "review" / "http.py",
            "ReviewHttpMixin",
        )
        self.assertEqual(set(alerts), ALERT_HTTP_METHODS)
        self.assertEqual(set(review), REVIEW_HTTP_METHODS)
        for path in (
            APP_ROOT / "backend" / "features" / "alerts" / "http.py",
            APP_ROOT / "backend" / "features" / "review" / "http.py",
        ):
            source = path.read_text(encoding="utf-8")
            self.assertNotIn("SERVICE.", source)
            self.assertIn("self.application_service.", source)


if __name__ == "__main__":
    unittest.main()
