"""Tests for the CQRS + Event Sourcing domain layer and MCP vallm integration."""
from __future__ import annotations

from types import SimpleNamespace

import pytest
from starlette.testclient import TestClient

from proxym.main import app


@pytest.fixture
def client():
    with TestClient(app) as c:
        yield c


@pytest.fixture
def authed_client():
    """TestClient with auth header — needed for /observability/* endpoints."""
    with TestClient(app) as c:
        c.headers["Authorization"] = "Bearer sk-proxy-local-dev"
        yield c


# ---------------------------------------------------------------------------
# Domain Events
# ---------------------------------------------------------------------------

class TestDomainEvents:
    """Test domain event store and observability endpoint."""

    def test_domain_events_endpoint_returns_200(self, authed_client):
        r = authed_client.get("/observability/domain-events")
        assert r.status_code == 200
        data = r.json()
        assert "total" in data
        assert "events" in data
        assert isinstance(data["events"], list)

    def test_project_creation_emits_event(self, authed_client):
        """Creating a project via API should emit a ProjectCreated event."""
        # Create project (dashboard endpoints don't need auth)
        r = authed_client.post("/dashboard/projects", json={"name": "test-cqrs-event"})
        assert r.status_code == 200

        # Check domain events
        r2 = authed_client.get("/observability/domain-events?event_type=ProjectCreated")
        data = r2.json()
        events = data["events"]
        assert any(
            e["event_type"] == "ProjectCreated" and e["data"].get("name") == "test-cqrs-event"
            for e in events
        )

    def test_project_scan_emits_event(self, authed_client, tmp_path):
        """Scanning a directory should emit a ProjectScanned event."""
        proj = tmp_path / "scan-proj"
        proj.mkdir()
        (proj / "README.md").write_text("# test")

        r = authed_client.post("/dashboard/projects/scan", json={"directory": str(tmp_path)})
        assert r.status_code == 200

        r2 = authed_client.get("/observability/domain-events?event_type=ProjectScanned")
        data = r2.json()
        events = data["events"]
        assert any(e["event_type"] == "ProjectScanned" for e in events)

    def test_event_has_actor_field(self, authed_client):
        """Events emitted from API should have actor='api'."""
        authed_client.post("/dashboard/projects", json={"name": "actor-test"})

        r = authed_client.get("/observability/domain-events")
        events = r.json()["events"]
        api_events = [e for e in events if e.get("actor") == "api"]
        assert len(api_events) > 0


# ---------------------------------------------------------------------------
# CQRS Command/Query Bus
# ---------------------------------------------------------------------------

class TestCQRSBus:
    """Test that commands and queries go through the bus."""

    def test_add_project_through_bus(self, client):
        """POST /dashboard/projects delegates to CommandBus."""
        r = client.post("/dashboard/projects", json={"name": "bus-test"})
        assert r.status_code == 200
        data = r.json()
        assert data["name"] == "bus-test"
        assert data["source_type"] == "local"

    def test_list_projects_through_bus(self, client, tmp_path):
        """GET /dashboard/projects delegates to QueryBus."""
        proj = tmp_path / "qbus-test"
        proj.mkdir()
        client.post("/dashboard/projects", json={"path": str(proj)})

        r = client.get("/dashboard/projects")
        assert r.status_code == 200
        projects = r.json()
        assert isinstance(projects, list)

    def test_list_projects_seeds_active_workspace(self, client, monkeypatch, tmp_path):
        """GET /dashboard/projects should include the active workspace even when registry is empty."""
        from proxym.dashboard import _projects_api as projects_api

        workspace_root = tmp_path / "reduplikacja"
        workspace_root.mkdir()
        (workspace_root / ".git").mkdir()
        (workspace_root / "pyproject.toml").write_text("[project]\nname='reduplikacja'\n")

        module_path = workspace_root / "src/proxym/dashboard/_projects_api.py"
        module_path.parent.mkdir(parents=True)
        module_path.write_text("# fake module path for workspace seeding\n")

        monkeypatch.setattr(projects_api, "__file__", str(module_path))
        monkeypatch.setattr(
            projects_api,
            "DEFAULT_SQLITE_PATH",
            tmp_path / "projects.sqlite",
        )
        monkeypatch.setattr(
            projects_api,
            "get_settings",
            lambda: SimpleNamespace(projects_root=str(tmp_path / "missing-projects-root")),
        )

        r = client.get("/dashboard/projects")
        assert r.status_code == 200
        projects = r.json()
        assert isinstance(projects, list)
        assert any(p["name"] == workspace_root.name for p in projects)

    def test_get_project_through_bus(self, client):
        """GET /dashboard/projects/{id} delegates to QueryBus."""
        r = client.post("/dashboard/projects", json={"name": "get-test"})
        pid = r.json()["id"]

        r2 = client.get(f"/dashboard/projects/{pid}")
        assert r2.status_code == 200
        assert r2.json()["name"] == "get-test"

    def test_update_project_through_bus(self, client):
        """PUT /dashboard/projects/{id} delegates to CommandBus."""
        r = client.post("/dashboard/projects", json={"name": "update-test"})
        pid = r.json()["id"]

        r2 = client.put(f"/dashboard/projects/{pid}", json={"description": "updated"})
        assert r2.status_code == 200
        assert r2.json()["description"] == "updated"

    def test_remove_project_through_bus(self, client):
        """DELETE /dashboard/projects/{id} delegates to CommandBus."""
        r = client.post("/dashboard/projects", json={"name": "remove-test"})
        pid = r.json()["id"]

        r2 = client.delete(f"/dashboard/projects/{pid}")
        assert r2.status_code == 200
        assert r2.json()["status"] == "deleted"

    def test_name_only_project_returns_200(self, client):
        """Name-only project creation should return 200 (not 400)."""
        r = client.post("/dashboard/projects", json={"name": "label-only"})
        assert r.status_code == 200
        assert r.json()["name"] == "label-only"

    def test_no_name_no_source_returns_400(self, client):
        """Empty request body should return 400."""
        r = client.post("/dashboard/projects", json={})
        assert r.status_code == 400


# ---------------------------------------------------------------------------
# MCP Vallm Integration
# ---------------------------------------------------------------------------

class TestMCPVallmIntegration:
    """Test vallm code validation exposed as MCP tools."""

    def test_validate_syntax_endpoint(self, client):
        """Syntax validation via MCP tool."""
        r = client.post("/mcp/self/tools/validate_syntax", json={
            "code": "x = 1\nprint(x)",
            "language": "python",
        })
        assert r.status_code == 200
        data = r.json()
        assert data["validator"] == "syntax"
        # Valid code should pass
        if "error" not in data:
            assert data["score"] >= 0.8

    def test_validate_syntax_bad_code(self, client):
        """Syntax validation detects errors."""
        r = client.post("/mcp/self/tools/validate_syntax", json={
            "code": "def foo(\n  x = ",
            "language": "python",
        })
        assert r.status_code == 200
        data = r.json()
        assert data["validator"] == "syntax"

    def test_validate_security_endpoint(self, client):
        """Security validation via MCP tool."""
        r = client.post("/mcp/self/tools/validate_security", json={
            "code": "eval(input())",
            "language": "python",
        })
        assert r.status_code == 200
        data = r.json()
        assert data["validator"] == "security"
        # eval() should be flagged
        if "error" not in data and data.get("issues"):
            assert any("eval" in i.get("message", "").lower() for i in data["issues"])

    def test_validate_code_pipeline(self, client):
        """Full validation pipeline via MCP tool."""
        r = client.post("/mcp/self/tools/validate_code", json={
            "code": "def hello():\n    return 'world'",
            "language": "python",
            "validators": ["syntax"],
        })
        assert r.status_code == 200
        data = r.json()
        assert "results" in data
        assert "aggregate_score" in data

    def test_mcp_tools_schema_includes_vallm(self, client):
        """The MCP tool schema should include vallm tools."""
        from proxym.mcp.self_server import TOOL_SCHEMA
        tool_names = [t["function"]["name"] for t in TOOL_SCHEMA]
        assert "validate_syntax" in tool_names
        assert "validate_security" in tool_names
        assert "validate_code" in tool_names
