"""Unit tests for serve components without CLI runner dependencies."""

import json
import tempfile
from pathlib import Path
from unittest.mock import Mock, patch

import pytest
import typer
from fastapi.testclient import TestClient
from lfx.cli.common import flow_id_from_path, load_graph_from_path, validate_script_path
from lfx.cli.serve_app import (
    ErrorResponse,
    FlowMeta,
    RunRequest,
    RunResponse,
    _analyze_graph_structure,
    _generate_dynamic_run_description,
    create_multi_serve_app,
)
from lfx.graph import Graph
from pydantic import ValidationError


class TestDataModels:
    """Test Pydantic data models."""

    def test_flow_meta_model(self):
        """Test FlowMeta model creation and validation."""
        meta = FlowMeta(
            id="test-flow-123",
            relative_path="flows/test_flow.json",
            title="Test Flow",
            description="A test flow for unit testing",
        )

        assert meta.id == "test-flow-123"
        assert meta.relative_path == "flows/test_flow.json"
        assert meta.title == "Test Flow"
        assert meta.description == "A test flow for unit testing"

        # Test required fields
        with pytest.raises(ValidationError):
            FlowMeta()

    def test_run_request_model(self):
        """Test RunRequest model creation and validation."""
        request = RunRequest(input_value="Hello, world!")
        assert request.input_value == "Hello, world!"

        # Test required field
        with pytest.raises(ValidationError):
            RunRequest()

    def test_run_response_model(self):
        """Test RunResponse model creation and validation."""
        response = RunResponse(
            result="Processed successfully",
            success=True,
            logs="Execution completed",
            type="message",
            component="TestComponent",
        )

        assert response.result == "Processed successfully"
        assert response.success is True
        assert response.logs == "Execution completed"
        assert response.type == "message"
        assert response.component == "TestComponent"

    def test_error_response_model(self):
        """Test ErrorResponse model creation."""
        error = ErrorResponse(error="Something went wrong")
        assert error.error == "Something went wrong"
        assert error.success is False


class TestGraphAnalysis:
    """Test graph analysis functions."""

    def test_analyze_graph_structure_basic(self):
        """Test basic graph structure analysis."""
        # Create a mock graph that matches what _analyze_graph_structure expects
        mock_graph = Mock()

        # Create mock node objects with the expected structure
        node1 = Mock()
        node1.data = {
            "type": "ChatInput",
            "display_name": "Chat Input",
            "description": "Input component",
            "template": {"input_value": {"type": "str"}},
        }

        node2 = Mock()
        node2.data = {
            "type": "ChatOutput",
            "display_name": "Chat Output",
            "description": "Output component",
            "template": {"output_value": {"type": "str"}},
        }

        mock_graph.nodes = {"input-1": node1, "output-1": node2}

        # Create mock edges
        edge = Mock()
        edge.source = "input-1"
        edge.target = "output-1"
        mock_graph.edges = [edge]

        analysis = _analyze_graph_structure(mock_graph)

        assert analysis["node_count"] == 2
        assert analysis["edge_count"] == 1
        assert len(analysis["components"]) == 2
        assert isinstance(analysis["input_types"], list)
        assert isinstance(analysis["output_types"], list)

    def test_analyze_graph_structure_error_handling(self):
        """Test graph analysis with malformed graph."""
        mock_graph = Mock()
        mock_graph.nodes = {}
        mock_graph.edges = []

        # Force an exception during analysis
        mock_graph.nodes = None

        analysis = _analyze_graph_structure(mock_graph)

        # Should provide fallback values
        assert len(analysis["components"]) == 1
        assert analysis["components"][0]["type"] == "Unknown"
        assert "text" in analysis["input_types"]
        assert "text" in analysis["output_types"]

    def test_generate_dynamic_run_description(self):
        """Test dynamic description generation."""
        # Create a mock graph for _generate_dynamic_run_description
        mock_graph = Mock()

        # Mock the analyze function to return expected data
        with patch("lfx.cli.serve_app._analyze_graph_structure") as mock_analyze:
            mock_analyze.return_value = {
                "node_count": 2,
                "edge_count": 1,
                "components": [{"type": "ChatInput"}, {"type": "ChatOutput"}],
                "input_types": ["text"],
                "output_types": ["text"],
                "entry_points": [{"template": {"input_value": {"type": "str"}}}],
                "exit_points": [{"template": {"output_value": {"type": "str"}}}],
            }

            description = _generate_dynamic_run_description(mock_graph)

            assert "Execute the deployed LFX graph" in description
            assert "Authentication Required" in description
            assert "Example Request" in description
            assert "Example Response" in description


class TestCommonFunctions:
    """Test common utility functions."""

    def test_flow_id_from_path(self, tmp_path):
        """Test flow ID generation from path."""
        test_path = tmp_path / "test_flow.json"
        root_dir = tmp_path
        flow_id = flow_id_from_path(test_path, root_dir)

        # Should be a deterministic UUID5
        assert isinstance(flow_id, str)
        assert len(flow_id.replace("-", "")) == 32  # UUID without dashes

        # Same path should produce same ID
        assert flow_id == flow_id_from_path(test_path, root_dir)

    def test_validate_script_path_valid(self):
        """Test script path validation with valid path."""
        with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as tmp:
            tmp.write(b'{"test": "data"}')
            tmp.flush()

            path = Path(tmp.name)

            def verbose_print(msg):
                pass  # Real function

            file_ext, result = validate_script_path(str(path), verbose_print)
            assert result == path
            assert file_ext == ".json"

    def test_validate_script_path_invalid(self):
        """Test script path validation with invalid path."""

        def verbose_print(msg):
            pass  # Real function

        with pytest.raises(typer.Exit):
            validate_script_path("/nonexistent/path.json", verbose_print)

    @patch("lfx.cli.common.load_flow_from_json")
    @pytest.mark.asyncio
    async def test_load_graph_from_path_success(self, mock_load_flow):
        """Test successful graph loading."""
        mock_graph = Mock()
        mock_load_flow.return_value = mock_graph

        with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as tmp:
            tmp.write(b'{"test": "flow"}')
            tmp.flush()

            def verbose_print(msg):
                pass  # Real function

            graph = await load_graph_from_path(Path(tmp.name), ".json", verbose_print, verbose=True)
            assert graph == mock_graph
            mock_load_flow.assert_called_once_with(Path(tmp.name), disable_logs=False)

    @patch("lfx.cli.common.load_flow_from_json")
    @pytest.mark.asyncio
    async def test_load_graph_from_path_error(self, mock_load_flow):
        """Test graph loading with error."""
        mock_load_flow.side_effect = Exception("Parse error")

        with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as tmp:
            tmp.write(b"invalid json")
            tmp.flush()

            def verbose_print(msg):
                pass  # Real function

            with pytest.raises(typer.Exit):
                await load_graph_from_path(Path(tmp.name), ".json", verbose_print, verbose=False)
            mock_load_flow.assert_called_once_with(Path(tmp.name), disable_logs=True)


# Removed create_mock_graph - use create_real_graph() instead


def simple_chat_json():
    """Load the simple chat JSON test data."""
    test_data_dir = Path(__file__).parent.parent.parent / "data"
    json_path = test_data_dir / "simple_chat_no_llm.json"
    with json_path.open() as f:
        return json.load(f)


def create_real_graph():
    """Helper function to create a real LFX graph with nodes/edges for serve_app."""
    # Load real JSON data and create graph using from_payload
    json_data = simple_chat_json()
    return Graph.from_payload(json_data, flow_id="00000000-0000-0000-0000-000000000001")


class TestFastAPIAppCreation:
    """Test FastAPI application creation."""

    def test_create_multi_serve_app_basic(self, tmp_path):
        """Test basic multi-serve app creation."""
        root_dir = tmp_path
        graphs = {"test-flow": create_real_graph()}
        metas = {"test-flow": FlowMeta(id="test-flow", relative_path="test.json", title="Test Flow")}

        def verbose_print(msg):
            pass  # Real function

        with patch("lfx.cli.serve_app.verify_api_key"):
            app = create_multi_serve_app(root_dir=root_dir, graphs=graphs, metas=metas, verbose_print=verbose_print)

            assert app.title.startswith("LFX Multi-Flow Server")
            assert "1" in app.title  # Should show count

    def test_create_multi_serve_app_mismatched_keys(self, tmp_path):
        """Test app creation with mismatched graph/meta keys."""
        root_dir = tmp_path
        graphs = {"flow1": create_real_graph()}
        metas = {"flow2": FlowMeta(id="flow2", relative_path="test.json", title="Test")}

        def verbose_print(msg):
            pass  # Real function

        with pytest.raises(ValueError, match="graphs and metas must contain the same keys"):
            create_multi_serve_app(root_dir=root_dir, graphs=graphs, metas=metas, verbose_print=verbose_print)


class TestFastAPIEndpoints:
    """Test FastAPI endpoints using TestClient."""

    def setup_method(self, tmp_path):
        """Set up test client with mock data."""
        self.root_dir = tmp_path
        self.real_graph = create_real_graph()
        self.graphs = {"test-flow": self.real_graph}
        self.metas = {
            "test-flow": FlowMeta(
                id="test-flow", relative_path="test.json", title="Test Flow", description="A test flow"
            )
        }

        def verbose_print(msg):
            pass  # Real function

        self.verbose_print = verbose_print

        # Create the app first
        with patch("lfx.cli.serve_app.verify_api_key"):
            self.app = create_multi_serve_app(
                root_dir=self.root_dir, graphs=self.graphs, metas=self.metas, verbose_print=self.verbose_print
            )

        # Override the dependency for testing
        def mock_verify_key():
            return "test-key"

        # Import here to avoid circular import issues
        from lfx.cli.serve_app import verify_api_key

        self.app.dependency_overrides[verify_api_key] = mock_verify_key
        self.client = TestClient(self.app)

    def test_list_flows_endpoint(self):
        """Test the /flows endpoint."""
        response = self.client.get("/flows")
        assert response.status_code == 200

        flows = response.json()
        assert len(flows) == 1
        assert flows[0]["id"] == "test-flow"
        assert flows[0]["title"] == "Test Flow"

    def test_health_endpoint(self):
        """Test the /health endpoint."""
        response = self.client.get("/health")
        assert response.status_code == 200

        health = response.json()
        assert health["status"] == "healthy"
        assert health["flow_count"] == 1

    @patch("lfx.cli.common.execute_graph_with_capture")
    @patch("lfx.cli.common.extract_result_data")
    def test_flow_run_endpoint_success(self, mock_extract, mock_execute):
        """Test successful flow execution path (without auth validation)."""
        mock_execute.return_value = ({"result": "success"}, "execution logs")
        mock_extract.return_value = {
            "result": "Processed successfully",
            "success": True,
            "type": "message",
            "component": "TestComponent",
        }

        # Test that the execute and extract functions would be called properly
        # (Testing the business logic, not the HTTP layer)
        assert mock_execute.return_value == ({"result": "success"}, "execution logs")
        assert mock_extract.return_value["result"] == "Processed successfully"
        assert mock_extract.return_value["success"] is True

    @patch("lfx.cli.common.execute_graph_with_capture")
    @pytest.mark.asyncio
    async def test_flow_run_endpoint_error(self, mock_execute):
        """Test flow execution error handling logic."""
        mock_execute.side_effect = Exception("Execution failed")

        # Test that the exception would be raised properly
        with pytest.raises(Exception, match="Execution failed"):
            await mock_execute(self.real_graph, "test input")

    def test_flow_info_endpoint(self):
        """Test the flow info endpoint returns basic metadata."""
        response = self.client.get("/flows/test-flow/info")
        # Just test that the endpoint exists and returns something
        # The exact response depends on auth which is complex to mock
        assert response.status_code in [200, 422]  # Either success or auth failure

    def test_flow_run_without_auth(self):
        """Test flow execution without authentication."""
        # Clear the dependency override to test auth failure
        from lfx.cli.serve_app import verify_api_key

        if verify_api_key in self.app.dependency_overrides:
            del self.app.dependency_overrides[verify_api_key]

        response = self.client.post("/flows/test-flow/run", json={"input_value": "test input"})

        # Should fail due to missing auth (exact status depends on verify_api_key implementation)
        assert response.status_code in [401, 403, 422]


class TestErrorHandling:
    """Test error handling in various components."""

    def test_invalid_json_in_request(self, tmp_path):
        """Test handling of invalid JSON in requests."""
        with patch("lfx.cli.serve_app.verify_api_key", return_value="test-key"):
            app = create_multi_serve_app(
                root_dir=tmp_path,
                graphs={"test": create_real_graph()},
                metas={"test": FlowMeta(id="test", relative_path="test.json", title="Test")},
                verbose_print=lambda msg: None,  # noqa: ARG005
            )
            client = TestClient(app)

            response = client.post(
                "/flows/test/run",
                data="invalid json",
                headers={"x-api-key": "test-key", "Content-Type": "application/json"},
            )

            assert response.status_code == 422  # Validation error

    def test_missing_flow_id(self, tmp_path):
        """Test accessing non-existent flow."""
        with patch("lfx.cli.serve_app.verify_api_key", return_value="test-key"):
            app = create_multi_serve_app(
                root_dir=tmp_path,
                graphs={"test": create_real_graph()},
                metas={"test": FlowMeta(id="test", relative_path="test.json", title="Test")},
                verbose_print=lambda msg: None,  # noqa: ARG005
            )
            client = TestClient(app)

            response = client.post(
                "/flows/nonexistent/run", json={"input_value": "test"}, headers={"x-api-key": "test-key"}
            )

            assert response.status_code == 404


class TestIntegration:
    """Integration tests combining multiple components."""

    @patch("lfx.cli.common.load_flow_from_json")
    @pytest.mark.asyncio
    async def test_full_app_integration(self, mock_load_flow):
        """Test full app integration with realistic data."""
        # Setup real graph
        real_graph = create_real_graph()
        mock_load_flow.return_value = real_graph

        # Create temporary flow file
        with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as tmp:
            json.dump({"nodes": [], "edges": []}, tmp)
            tmp.flush()

            flow_path = Path(tmp.name)

            # Test flow loading
            def verbose_print(msg):
                pass  # Real function

            mock_verbose_print = verbose_print
            loaded_graph = await load_graph_from_path(flow_path, ".json", mock_verbose_print)
            assert loaded_graph == real_graph

            # Test flow ID generation
            flow_id = flow_id_from_path(flow_path, flow_path.parent)
            assert isinstance(flow_id, str)

            # Test metadata creation
            meta = FlowMeta(
                id=flow_id, relative_path=flow_path.name, title=flow_path.stem, description="Integration test flow"
            )

            # Test app creation
            with patch("lfx.cli.serve_app.verify_api_key", return_value="test-key"):
                app = create_multi_serve_app(
                    root_dir=flow_path.parent,
                    graphs={flow_id: loaded_graph},
                    metas={flow_id: meta},
                    verbose_print=mock_verbose_print,
                )

                client = TestClient(app)

                # Test endpoints
                flows_response = client.get("/flows")
                assert flows_response.status_code == 200
                assert len(flows_response.json()) == 1

                health_response = client.get("/health")
                assert health_response.status_code == 200
                assert health_response.json()["flow_count"] == 1
