"""Integration tests for session endpoints."""

import pytest
from httpx import AsyncClient


class TestSessions:
    """Test suite for troubleshooting session endpoints."""

    @pytest.mark.asyncio
    async def test_create_session(
        self, client: AsyncClient, auth_headers: dict, test_tree: dict
    ):
        """Test starting a new troubleshooting session."""
        session_data = {
            "tree_id": test_tree["id"],
            "ticket_number": "TICKET-123",
            "client_name": "Test Client"
        }

        response = await client.post(
            "/api/v1/sessions",
            json=session_data,
            headers=auth_headers
        )

        assert response.status_code == 201
        data = response.json()
        assert data["tree_id"] == test_tree["id"]
        assert data["ticket_number"] == session_data["ticket_number"]
        assert data["client_name"] == session_data["client_name"]
        assert data["path_taken"] == []
        assert data["decisions"] == []
        assert data["completed_at"] is None
        assert "id" in data
        assert "started_at" in data

    @pytest.mark.asyncio
    async def test_get_session(
        self, client: AsyncClient, auth_headers: dict, test_tree: dict
    ):
        """Test retrieving a specific session."""
        # Create a session first
        create_response = await client.post(
            "/api/v1/sessions",
            json={"tree_id": test_tree["id"]},
            headers=auth_headers
        )
        session_id = create_response.json()["id"]

        # Get the session
        response = await client.get(
            f"/api/v1/sessions/{session_id}",
            headers=auth_headers
        )

        assert response.status_code == 200
        data = response.json()
        assert data["id"] == session_id
        assert data["tree_id"] == test_tree["id"]

    @pytest.mark.asyncio
    async def test_list_sessions(
        self, client: AsyncClient, auth_headers: dict, test_tree: dict
    ):
        """Test listing user's sessions."""
        # Create a session
        await client.post(
            "/api/v1/sessions",
            json={"tree_id": test_tree["id"]},
            headers=auth_headers
        )

        # List sessions
        response = await client.get("/api/v1/sessions", headers=auth_headers)

        assert response.status_code == 200
        data = response.json()
        assert isinstance(data, list)
        assert len(data) >= 1

    @pytest.mark.asyncio
    async def test_update_session_path(
        self, client: AsyncClient, auth_headers: dict, test_tree: dict
    ):
        """Test updating session with path taken."""
        # Create session
        create_response = await client.post(
            "/api/v1/sessions",
            json={"tree_id": test_tree["id"]},
            headers=auth_headers
        )
        session_id = create_response.json()["id"]

        # Update path
        update_data = {
            "path_taken": ["root", "solution1"]
        }

        response = await client.put(
            f"/api/v1/sessions/{session_id}",
            json=update_data,
            headers=auth_headers
        )

        assert response.status_code == 200
        data = response.json()
        assert data["path_taken"] == update_data["path_taken"]

    @pytest.mark.asyncio
    async def test_update_session_ticket(
        self, client: AsyncClient, auth_headers: dict, test_tree: dict
    ):
        """Test updating session metadata."""
        # Create session
        create_response = await client.post(
            "/api/v1/sessions",
            json={"tree_id": test_tree["id"]},
            headers=auth_headers
        )
        session_id = create_response.json()["id"]

        # Update metadata
        update_data = {
            "ticket_number": "UPDATED-456",
            "client_name": "Updated Client"
        }

        response = await client.put(
            f"/api/v1/sessions/{session_id}",
            json=update_data,
            headers=auth_headers
        )

        assert response.status_code == 200
        data = response.json()
        assert data["ticket_number"] == update_data["ticket_number"]
        assert data["client_name"] == update_data["client_name"]

    @pytest.mark.asyncio
    async def test_complete_session(
        self, client: AsyncClient, auth_headers: dict, test_tree: dict
    ):
        """Test marking a session as complete."""
        # Create session
        create_response = await client.post(
            "/api/v1/sessions",
            json={"tree_id": test_tree["id"]},
            headers=auth_headers
        )
        session_id = create_response.json()["id"]

        # Complete session
        response = await client.post(
            f"/api/v1/sessions/{session_id}/complete",
            json={"outcome": "resolved", "outcome_notes": "Issue fixed after restarting service"},
            headers=auth_headers
        )

        assert response.status_code == 200
        data = response.json()
        assert data["completed_at"] is not None
        assert data["outcome"] == "resolved"
        assert data["outcome_notes"] == "Issue fixed after restarting service"

    @pytest.mark.asyncio
    async def test_complete_session_requires_outcome(
        self, client: AsyncClient, auth_headers: dict, test_tree: dict
    ):
        """Test that completion requires an outcome payload."""
        create_response = await client.post(
            "/api/v1/sessions",
            json={"tree_id": test_tree["id"]},
            headers=auth_headers
        )
        session_id = create_response.json()["id"]

        response = await client.post(
            f"/api/v1/sessions/{session_id}/complete",
            headers=auth_headers
        )

        assert response.status_code == 422

    @pytest.mark.asyncio
    async def test_complete_already_completed_session(
        self, client: AsyncClient, auth_headers: dict, test_tree: dict
    ):
        """Test that completing an already completed session fails."""
        # Create and complete session
        create_response = await client.post(
            "/api/v1/sessions",
            json={"tree_id": test_tree["id"]},
            headers=auth_headers
        )
        session_id = create_response.json()["id"]

        await client.post(
            f"/api/v1/sessions/{session_id}/complete",
            json={"outcome": "resolved"},
            headers=auth_headers
        )

        # Try to complete again
        response = await client.post(
            f"/api/v1/sessions/{session_id}/complete",
            json={"outcome": "resolved"},
            headers=auth_headers
        )

        assert response.status_code == 400

    @pytest.mark.asyncio
    async def test_export_session_markdown(
        self, client: AsyncClient, auth_headers: dict, test_tree: dict
    ):
        """Test exporting session to markdown format."""
        # Create session with ticket number
        create_response = await client.post(
            "/api/v1/sessions",
            json={"tree_id": test_tree["id"], "ticket_number": "EXP-001"},
            headers=auth_headers
        )
        session_id = create_response.json()["id"]

        # Export as markdown
        export_data = {
            "format": "markdown",
            "include_timestamps": True,
            "include_tree_info": True
        }

        response = await client.post(
            f"/api/v1/sessions/{session_id}/export",
            json=export_data,
            headers=auth_headers
        )

        assert response.status_code == 200
        content = response.text
        assert "EXP-001" in content  # Should contain ticket number
        assert "#" in content  # Markdown headers

    @pytest.mark.asyncio
    async def test_export_markdown_includes_outcome_and_step_duration(
        self, client: AsyncClient, auth_headers: dict, test_tree: dict
    ):
        """Test markdown export includes session outcome and per-step duration."""
        create_response = await client.post(
            "/api/v1/sessions",
            json={"tree_id": test_tree["id"], "ticket_number": "EXP-OUTCOME-001"},
            headers=auth_headers
        )
        session_id = create_response.json()["id"]

        step_timestamp = "2026-02-11T10:10:00Z"
        update_response = await client.put(
            f"/api/v1/sessions/{session_id}",
            json={
                "decisions": [{
                    "node_id": "root",
                    "question": "Is this a test?",
                    "answer": "Yes",
                    "action_performed": None,
                    "notes": "Validated quickly",
                    "automation_used": False,
                    "timestamp": step_timestamp,
                    "entered_at": "2026-02-11T10:08:30Z",
                    "exited_at": step_timestamp,
                    "duration_seconds": 90,
                    "attachments": []
                }]
            },
            headers=auth_headers
        )
        assert update_response.status_code == 200

        complete_response = await client.post(
            f"/api/v1/sessions/{session_id}/complete",
            json={"outcome": "workaround", "outcome_notes": "Temporary mitigation applied"},
            headers=auth_headers
        )
        assert complete_response.status_code == 200

        export_response = await client.post(
            f"/api/v1/sessions/{session_id}/export",
            json={"format": "markdown", "include_timestamps": True, "include_tree_info": True},
            headers=auth_headers
        )

        assert export_response.status_code == 200
        content = export_response.text
        assert "**Outcome:** Workaround Applied" in content
        assert "**Duration:**" in content
        assert "**Duration:** 1m 30s" in content

    @pytest.mark.asyncio
    async def test_export_session_text(
        self, client: AsyncClient, auth_headers: dict, test_tree: dict
    ):
        """Test exporting session to text format."""
        # Create session
        create_response = await client.post(
            "/api/v1/sessions",
            json={"tree_id": test_tree["id"]},
            headers=auth_headers
        )
        session_id = create_response.json()["id"]

        # Export as text
        export_data = {"format": "text"}

        response = await client.post(
            f"/api/v1/sessions/{session_id}/export",
            json=export_data,
            headers=auth_headers
        )

        assert response.status_code == 200
        assert response.headers["content-type"] == "text/plain; charset=utf-8"

    @pytest.mark.asyncio
    async def test_export_session_html(
        self, client: AsyncClient, auth_headers: dict, test_tree: dict
    ):
        """Test exporting session to HTML format."""
        # Create session
        create_response = await client.post(
            "/api/v1/sessions",
            json={"tree_id": test_tree["id"]},
            headers=auth_headers
        )
        session_id = create_response.json()["id"]

        # Export as HTML
        export_data = {"format": "html"}

        response = await client.post(
            f"/api/v1/sessions/{session_id}/export",
            json=export_data,
            headers=auth_headers
        )

        assert response.status_code == 200
        content = response.text
        assert "<!DOCTYPE html>" in content
        assert "<html>" in content

    @pytest.mark.asyncio
    async def test_filter_sessions_by_completion(
        self, client: AsyncClient, auth_headers: dict, test_tree: dict
    ):
        """Test filtering sessions by completion status."""
        # Create two sessions, complete one
        create1 = await client.post(
            "/api/v1/sessions",
            json={"tree_id": test_tree["id"]},
            headers=auth_headers
        )
        session1_id = create1.json()["id"]

        await client.post(
            "/api/v1/sessions",
            json={"tree_id": test_tree["id"]},
            headers=auth_headers
        )

        # Complete first session
        await client.post(
            f"/api/v1/sessions/{session1_id}/complete",
            json={"outcome": "resolved"},
            headers=auth_headers
        )

        # Get completed sessions
        response = await client.get(
            "/api/v1/sessions?completed=true",
            headers=auth_headers
        )

        assert response.status_code == 200
        data = response.json()
        assert len(data) >= 1
        assert all(s["completed_at"] is not None for s in data)

        # Get incomplete sessions
        response = await client.get(
            "/api/v1/sessions?completed=false",
            headers=auth_headers
        )

        assert response.status_code == 200
        data = response.json()
        assert len(data) >= 1
        assert all(s["completed_at"] is None for s in data)

    @pytest.mark.asyncio
    async def test_create_session_has_scratchpad(
        self, client: AsyncClient, auth_headers: dict, test_tree: dict
    ):
        """Test that new sessions include scratchpad field."""
        session_data = {
            "tree_id": test_tree["id"],
        }

        response = await client.post(
            "/api/v1/sessions",
            json=session_data,
            headers=auth_headers
        )

        assert response.status_code == 201
        data = response.json()
        assert "scratchpad" in data
        assert data["scratchpad"] == ""

    @pytest.mark.asyncio
    async def test_update_scratchpad_via_put(
        self, client: AsyncClient, auth_headers: dict, test_tree: dict
    ):
        """Test updating scratchpad through the existing PUT endpoint."""
        # Create session
        create_response = await client.post(
            "/api/v1/sessions",
            json={"tree_id": test_tree["id"]},
            headers=auth_headers
        )
        session_id = create_response.json()["id"]

        # Update scratchpad via PUT
        update_data = {
            "scratchpad": "- Server IP: 192.168.1.50\n- Error: 0x80070005"
        }

        response = await client.put(
            f"/api/v1/sessions/{session_id}",
            json=update_data,
            headers=auth_headers
        )

        assert response.status_code == 200
        data = response.json()
        assert data["scratchpad"] == update_data["scratchpad"]

    @pytest.mark.asyncio
    async def test_patch_scratchpad(
        self, client: AsyncClient, auth_headers: dict, test_tree: dict
    ):
        """Test the dedicated PATCH scratchpad endpoint."""
        # Create session
        create_response = await client.post(
            "/api/v1/sessions",
            json={"tree_id": test_tree["id"]},
            headers=auth_headers
        )
        session_id = create_response.json()["id"]

        # Patch scratchpad
        response = await client.patch(
            f"/api/v1/sessions/{session_id}/scratchpad",
            json={"scratchpad": "- IP: 10.0.0.1\n- User: jsmith"},
            headers=auth_headers
        )

        assert response.status_code == 200
        data = response.json()
        assert data["scratchpad"] == "- IP: 10.0.0.1\n- User: jsmith"

    @pytest.mark.asyncio
    async def test_patch_scratchpad_not_found(
        self, client: AsyncClient, auth_headers: dict
    ):
        """Test PATCH scratchpad with invalid session ID."""
        import uuid
        fake_id = str(uuid.uuid4())

        response = await client.patch(
            f"/api/v1/sessions/{fake_id}/scratchpad",
            json={"scratchpad": "test"},
            headers=auth_headers
        )

        assert response.status_code == 404

    @pytest.mark.asyncio
    async def test_patch_scratchpad_empty_string(
        self, client: AsyncClient, auth_headers: dict, test_tree: dict
    ):
        """Test PATCH scratchpad with empty string (clear scratchpad)."""
        # Create session and set scratchpad
        create_response = await client.post(
            "/api/v1/sessions",
            json={"tree_id": test_tree["id"]},
            headers=auth_headers
        )
        session_id = create_response.json()["id"]

        # Set scratchpad
        await client.patch(
            f"/api/v1/sessions/{session_id}/scratchpad",
            json={"scratchpad": "some notes"},
            headers=auth_headers
        )

        # Clear scratchpad
        response = await client.patch(
            f"/api/v1/sessions/{session_id}/scratchpad",
            json={"scratchpad": ""},
            headers=auth_headers
        )

        assert response.status_code == 200
        assert response.json()["scratchpad"] == ""

    @pytest.mark.asyncio
    async def test_patch_scratchpad_completed_session(
        self, client: AsyncClient, auth_headers: dict, test_tree: dict
    ):
        """Test that scratchpad can still be updated on completed sessions."""
        # Create and complete session
        create_response = await client.post(
            "/api/v1/sessions",
            json={"tree_id": test_tree["id"]},
            headers=auth_headers
        )
        session_id = create_response.json()["id"]

        await client.post(
            f"/api/v1/sessions/{session_id}/complete",
            json={"outcome": "resolved"},
            headers=auth_headers
        )

        # Should still be able to update scratchpad on completed sessions
        response = await client.patch(
            f"/api/v1/sessions/{session_id}/scratchpad",
            json={"scratchpad": "post-resolution notes"},
            headers=auth_headers
        )

        assert response.status_code == 200
        assert response.json()["scratchpad"] == "post-resolution notes"

    @pytest.mark.asyncio
    async def test_export_includes_scratchpad_markdown(
        self, client: AsyncClient, auth_headers: dict, test_tree: dict
    ):
        """Test that markdown export includes scratchpad content."""
        # Create session with scratchpad
        create_response = await client.post(
            "/api/v1/sessions",
            json={"tree_id": test_tree["id"], "ticket_number": "SP-001"},
            headers=auth_headers
        )
        session_id = create_response.json()["id"]

        # Add scratchpad content
        await client.patch(
            f"/api/v1/sessions/{session_id}/scratchpad",
            json={"scratchpad": "- Server IP: 192.168.1.50\n- Error: 0x80070005"},
            headers=auth_headers
        )

        # Export as markdown
        response = await client.post(
            f"/api/v1/sessions/{session_id}/export",
            json={"format": "markdown", "include_tree_info": True},
            headers=auth_headers
        )

        assert response.status_code == 200
        content = response.text
        assert "## Evidence / Reference" in content
        assert "192.168.1.50" in content
        assert "0x80070005" in content

    @pytest.mark.asyncio
    async def test_export_includes_scratchpad_text(
        self, client: AsyncClient, auth_headers: dict, test_tree: dict
    ):
        """Test that text export includes scratchpad content."""
        create_response = await client.post(
            "/api/v1/sessions",
            json={"tree_id": test_tree["id"]},
            headers=auth_headers
        )
        session_id = create_response.json()["id"]

        await client.patch(
            f"/api/v1/sessions/{session_id}/scratchpad",
            json={"scratchpad": "Error code: 12345"},
            headers=auth_headers
        )

        response = await client.post(
            f"/api/v1/sessions/{session_id}/export",
            json={"format": "text"},
            headers=auth_headers
        )

        assert response.status_code == 200
        content = response.text
        assert "EVIDENCE / REFERENCE" in content
        assert "Error code: 12345" in content

    @pytest.mark.asyncio
    async def test_export_includes_scratchpad_html(
        self, client: AsyncClient, auth_headers: dict, test_tree: dict
    ):
        """Test that HTML export includes scratchpad content."""
        create_response = await client.post(
            "/api/v1/sessions",
            json={"tree_id": test_tree["id"]},
            headers=auth_headers
        )
        session_id = create_response.json()["id"]

        await client.patch(
            f"/api/v1/sessions/{session_id}/scratchpad",
            json={"scratchpad": "DNS server: 10.0.0.5"},
            headers=auth_headers
        )

        response = await client.post(
            f"/api/v1/sessions/{session_id}/export",
            json={"format": "html"},
            headers=auth_headers
        )

        assert response.status_code == 200
        content = response.text
        assert "Evidence / Reference" in content
        assert "DNS server: 10.0.0.5" in content

    @pytest.mark.asyncio
    async def test_export_excludes_empty_scratchpad(
        self, client: AsyncClient, auth_headers: dict, test_tree: dict
    ):
        """Test that export omits scratchpad section when empty."""
        create_response = await client.post(
            "/api/v1/sessions",
            json={"tree_id": test_tree["id"]},
            headers=auth_headers
        )
        session_id = create_response.json()["id"]

        # Export without setting scratchpad
        response = await client.post(
            f"/api/v1/sessions/{session_id}/export",
            json={"format": "markdown"},
            headers=auth_headers
        )

        assert response.status_code == 200
        content = response.text
        assert "Evidence / Reference" not in content

    @pytest.mark.asyncio
    async def test_export_excludes_whitespace_only_scratchpad(
        self, client: AsyncClient, auth_headers: dict, test_tree: dict
    ):
        """Test that export omits scratchpad section when only whitespace."""
        create_response = await client.post(
            "/api/v1/sessions",
            json={"tree_id": test_tree["id"]},
            headers=auth_headers
        )
        session_id = create_response.json()["id"]

        await client.patch(
            f"/api/v1/sessions/{session_id}/scratchpad",
            json={"scratchpad": "   \n  \n  "},
            headers=auth_headers
        )

        response = await client.post(
            f"/api/v1/sessions/{session_id}/export",
            json={"format": "markdown"},
            headers=auth_headers
        )

        assert response.status_code == 200
        content = response.text
        assert "Evidence / Reference" not in content

    @pytest.mark.asyncio
    async def test_html_export_escapes_script_tags(
        self, client: AsyncClient, auth_headers: dict, test_tree: dict
    ):
        """Test that HTML export escapes script tags in user content (XSS prevention)."""
        session_data = {
            "tree_id": test_tree["id"],
            "ticket_number": '<script>alert("xss")</script>',
            "client_name": '<img onerror="alert(1)" src=x>'
        }
        create_response = await client.post(
            "/api/v1/sessions", json=session_data, headers=auth_headers
        )
        session_id = create_response.json()["id"]

        response = await client.post(
            f"/api/v1/sessions/{session_id}/export",
            json={"format": "html", "include_tree_info": True},
            headers=auth_headers
        )

        assert response.status_code == 200
        content = response.text
        assert '<script>' not in content
        assert '&lt;script&gt;' in content
        assert '&lt;img' in content

    @pytest.mark.asyncio
    async def test_html_export_escapes_special_chars(
        self, client: AsyncClient, auth_headers: dict, test_tree: dict
    ):
        """Test that HTML export properly escapes special characters."""
        session_data = {
            "tree_id": test_tree["id"],
            "ticket_number": 'TICK-001 <b>"bold"</b> & \'quoted\''
        }
        create_response = await client.post(
            "/api/v1/sessions", json=session_data, headers=auth_headers
        )
        session_id = create_response.json()["id"]

        response = await client.post(
            f"/api/v1/sessions/{session_id}/export",
            json={"format": "html", "include_tree_info": True},
            headers=auth_headers
        )

        assert response.status_code == 200
        content = response.text
        assert '&amp;' in content
        assert '&lt;b&gt;' in content

    @pytest.mark.asyncio
    async def test_html_export_escapes_scratchpad(
        self, client: AsyncClient, auth_headers: dict, test_tree: dict
    ):
        """Test that HTML export escapes scratchpad content."""
        create_response = await client.post(
            "/api/v1/sessions",
            json={"tree_id": test_tree["id"]},
            headers=auth_headers
        )
        session_id = create_response.json()["id"]

        await client.patch(
            f"/api/v1/sessions/{session_id}/scratchpad",
            json={"scratchpad": '<script>document.cookie</script>'},
            headers=auth_headers
        )

        response = await client.post(
            f"/api/v1/sessions/{session_id}/export",
            json={"format": "html"},
            headers=auth_headers
        )

        assert response.status_code == 200
        content = response.text
        assert '<script>' not in content
        assert '&lt;script&gt;' in content

    @pytest.mark.asyncio
    async def test_start_session_on_others_private_tree_forbidden(
        self, client: AsyncClient, auth_headers: dict, test_tree: dict
    ):
        """Test that a user cannot start a session on another user's private tree."""
        # Register a second user
        await client.post("/api/v1/auth/register", json={
            "email": "other@example.com",
            "password": "OtherPassword123!",
            "name": "Other User"
        })
        login_resp = await client.post("/api/v1/auth/login/json", json={
            "email": "other@example.com",
            "password": "OtherPassword123!"
        })
        other_headers = {"Authorization": f"Bearer {login_resp.json()['access_token']}"}

        # test_tree is owned by test_user (not public, not default)
        response = await client.post(
            "/api/v1/sessions",
            json={"tree_id": test_tree["id"]},
            headers=other_headers
        )
        assert response.status_code == 403

    @pytest.mark.asyncio
    async def test_start_session_super_admin_any_tree(
        self, client: AsyncClient, admin_auth_headers: dict, test_tree: dict
    ):
        """Test that a super admin can start a session on any tree."""
        response = await client.post(
            "/api/v1/sessions",
            json={"tree_id": test_tree["id"]},
            headers=admin_auth_headers
        )
        assert response.status_code == 201

    @pytest.mark.asyncio
    async def test_filter_sessions_by_ticket_number(
        self, client: AsyncClient, auth_headers: dict, test_tree: dict
    ):
        """Test filtering sessions by ticket number (partial match)."""
        # Create sessions with different ticket numbers
        await client.post(
            "/api/v1/sessions",
            json={"tree_id": test_tree["id"], "ticket_number": "INC-12345"},
            headers=auth_headers
        )
        await client.post(
            "/api/v1/sessions",
            json={"tree_id": test_tree["id"], "ticket_number": "REQ-67890"},
            headers=auth_headers
        )

        # Filter by ticket number
        response = await client.get(
            "/api/v1/sessions?ticket_number=INC",
            headers=auth_headers
        )

        assert response.status_code == 200
        data = response.json()
        assert len(data) == 1
        assert data[0]["ticket_number"] == "INC-12345"

    @pytest.mark.asyncio
    async def test_filter_sessions_by_client_name(
        self, client: AsyncClient, auth_headers: dict, test_tree: dict
    ):
        """Test filtering sessions by client name (partial match, case-insensitive)."""
        # Create sessions with different clients
        await client.post(
            "/api/v1/sessions",
            json={"tree_id": test_tree["id"], "client_name": "Acme Corporation"},
            headers=auth_headers
        )
        await client.post(
            "/api/v1/sessions",
            json={"tree_id": test_tree["id"], "client_name": "TechStart Inc"},
            headers=auth_headers
        )

        # Filter by client name (case-insensitive partial match)
        response = await client.get(
            "/api/v1/sessions?client_name=acme",
            headers=auth_headers
        )

        assert response.status_code == 200
        data = response.json()
        assert len(data) == 1
        assert data[0]["client_name"] == "Acme Corporation"

    @pytest.mark.asyncio
    async def test_filter_sessions_by_tree_name(
        self, client: AsyncClient, auth_headers: dict, test_tree: dict
    ):
        """Test filtering sessions by tree name from snapshot."""
        # Create session (tree_snapshot includes tree name)
        response = await client.post(
            "/api/v1/sessions",
            json={"tree_id": test_tree["id"]},
            headers=auth_headers
        )
        assert response.status_code == 201

        # Filter by tree name (partial match from snapshot)
        tree_name_part = test_tree["name"][:5]  # First 5 chars
        response = await client.get(
            f"/api/v1/sessions?tree_name={tree_name_part}",
            headers=auth_headers
        )

        assert response.status_code == 200
        data = response.json()
        assert len(data) >= 1
        assert test_tree["name"] in data[0]["tree_snapshot"]["name"]

    @pytest.mark.asyncio
    async def test_filter_sessions_by_tree_id(
        self, client: AsyncClient, auth_headers: dict, test_tree: dict
    ):
        """Test filtering sessions by tree_id."""
        other_tree_response = await client.post(
            "/api/v1/trees",
            json={
                "name": "Other Tree",
                "description": "Second tree for filter test",
                "category": test_tree["category"],
                "tree_structure": test_tree["tree_structure"],
            },
            headers=auth_headers,
        )
        assert other_tree_response.status_code == 201
        other_tree_id = other_tree_response.json()["id"]

        await client.post(
            "/api/v1/sessions",
            json={"tree_id": test_tree["id"], "ticket_number": "TREE-A"},
            headers=auth_headers,
        )
        await client.post(
            "/api/v1/sessions",
            json={"tree_id": other_tree_id, "ticket_number": "TREE-B"},
            headers=auth_headers,
        )

        response = await client.get(
            f"/api/v1/sessions?tree_id={test_tree['id']}",
            headers=auth_headers,
        )
        assert response.status_code == 200
        data = response.json()
        assert len(data) >= 1
        assert all(item["tree_id"] == test_tree["id"] for item in data)

    @pytest.mark.asyncio
    async def test_list_sessions_supports_size_and_page_params(
        self, client: AsyncClient, auth_headers: dict, test_tree: dict
    ):
        """Test frontend-compatible page/size query params."""
        await client.post(
            "/api/v1/sessions",
            json={"tree_id": test_tree["id"], "ticket_number": "P1"},
            headers=auth_headers,
        )
        await client.post(
            "/api/v1/sessions",
            json={"tree_id": test_tree["id"], "ticket_number": "P2"},
            headers=auth_headers,
        )
        await client.post(
            "/api/v1/sessions",
            json={"tree_id": test_tree["id"], "ticket_number": "P3"},
            headers=auth_headers,
        )

        first_page = await client.get("/api/v1/sessions?size=2&page=1", headers=auth_headers)
        assert first_page.status_code == 200
        first_data = first_page.json()
        assert len(first_data) == 2

        second_page = await client.get("/api/v1/sessions?size=2&page=2", headers=auth_headers)
        assert second_page.status_code == 200
        second_data = second_page.json()
        assert len(second_data) >= 1

        first_ids = {item["id"] for item in first_data}
        second_ids = {item["id"] for item in second_data}
        assert first_ids.isdisjoint(second_ids)

    @pytest.mark.asyncio
    async def test_filter_sessions_by_started_date_range(
        self, client: AsyncClient, auth_headers: dict, test_tree: dict
    ):
        """Test filtering sessions by started date range."""
        from datetime import datetime, timezone, timedelta
        from urllib.parse import quote

        # Create a session
        response = await client.post(
            "/api/v1/sessions",
            json={"tree_id": test_tree["id"], "ticket_number": "TEST-001"},
            headers=auth_headers
        )
        assert response.status_code == 201

        # Get current time and create date range
        now = datetime.now(timezone.utc)
        yesterday = now - timedelta(days=1)
        tomorrow = now + timedelta(days=1)

        # Filter by started date range (should include the session)
        # URL encode the datetime strings
        started_after = quote(yesterday.isoformat())
        started_before = quote(tomorrow.isoformat())

        response = await client.get(
            f"/api/v1/sessions?started_after={started_after}&started_before={started_before}",
            headers=auth_headers
        )

        assert response.status_code == 200
        data = response.json()
        assert len(data) >= 1
        assert data[0]["ticket_number"] == "TEST-001"

    @pytest.mark.asyncio
    async def test_filter_sessions_by_completed_date_range(
        self, client: AsyncClient, auth_headers: dict, test_tree: dict
    ):
        """Test filtering sessions by completed date range."""
        from datetime import datetime, timezone, timedelta
        from urllib.parse import quote

        # Create and complete a session
        create_response = await client.post(
            "/api/v1/sessions",
            json={"tree_id": test_tree["id"], "ticket_number": "TEST-002"},
            headers=auth_headers
        )
        session_id = create_response.json()["id"]

        await client.post(
            f"/api/v1/sessions/{session_id}/complete",
            json={"outcome": "resolved"},
            headers=auth_headers
        )

        # Get current time and create date range
        now = datetime.now(timezone.utc)
        yesterday = now - timedelta(days=1)
        tomorrow = now + timedelta(days=1)

        # Filter by completed date range
        completed_after = quote(yesterday.isoformat())
        completed_before = quote(tomorrow.isoformat())

        response = await client.get(
            f"/api/v1/sessions?completed_after={completed_after}&completed_before={completed_before}",
            headers=auth_headers
        )

        assert response.status_code == 200
        data = response.json()
        assert len(data) >= 1
        assert any(s["ticket_number"] == "TEST-002" for s in data)

    @pytest.mark.asyncio
    async def test_filter_sessions_combined(
        self, client: AsyncClient, auth_headers: dict, test_tree: dict
    ):
        """Test combining multiple filters (AND logic)."""
        # Create sessions with various attributes
        await client.post(
            "/api/v1/sessions",
            json={
                "tree_id": test_tree["id"],
                "ticket_number": "INC-111",
                "client_name": "Client A"
            },
            headers=auth_headers
        )
        await client.post(
            "/api/v1/sessions",
            json={
                "tree_id": test_tree["id"],
                "ticket_number": "INC-222",
                "client_name": "Client B"
            },
            headers=auth_headers
        )

        # Filter by both ticket and client (should match only one)
        response = await client.get(
            "/api/v1/sessions?ticket_number=INC-111&client_name=Client A",
            headers=auth_headers
        )

        assert response.status_code == 200
        data = response.json()
        assert len(data) == 1
        assert data[0]["ticket_number"] == "INC-111"
        assert data[0]["client_name"] == "Client A"

    @pytest.mark.asyncio
    async def test_filter_sessions_no_results(
        self, client: AsyncClient, auth_headers: dict, test_tree: dict
    ):
        """Test that filtering returns empty list when no matches."""
        response = await client.get(
            "/api/v1/sessions?ticket_number=NONEXISTENT-999",
            headers=auth_headers
        )

        assert response.status_code == 200
        data = response.json()
        assert isinstance(data, list)
        assert len(data) == 0

    @pytest.mark.asyncio
    async def test_complete_session_with_next_steps(
        self, client: AsyncClient, auth_headers: dict, test_tree: dict
    ):
        """Test completing session saves next_steps."""
        create_response = await client.post(
            "/api/v1/sessions",
            json={"tree_id": test_tree["id"]},
            headers=auth_headers
        )
        session_id = create_response.json()["id"]

        response = await client.post(
            f"/api/v1/sessions/{session_id}/complete",
            json={
                "outcome": "resolved",
                "outcome_notes": "Fixed the issue",
                "next_steps": "Monitor for 48 hours"
            },
            headers=auth_headers
        )

        assert response.status_code == 200
        data = response.json()
        assert data["next_steps"] == "Monitor for 48 hours"
        assert data["outcome_notes"] == "Fixed the issue"

    @pytest.mark.asyncio
    async def test_update_session_next_steps(
        self, client: AsyncClient, auth_headers: dict, test_tree: dict
    ):
        """Test updating next_steps via session update."""
        create_response = await client.post(
            "/api/v1/sessions",
            json={"tree_id": test_tree["id"]},
            headers=auth_headers
        )
        session_id = create_response.json()["id"]

        response = await client.put(
            f"/api/v1/sessions/{session_id}",
            json={"next_steps": "Schedule follow-up call"},
            headers=auth_headers
        )

        assert response.status_code == 200
        assert response.json()["next_steps"] == "Schedule follow-up call"

    @pytest.mark.asyncio
    async def test_export_includes_outcome_notes_in_resolution(
        self, client: AsyncClient, auth_headers: dict, test_tree: dict
    ):
        """Test that outcome_notes appear as Resolution section in exports."""
        create_response = await client.post(
            "/api/v1/sessions",
            json={"tree_id": test_tree["id"]},
            headers=auth_headers
        )
        session_id = create_response.json()["id"]

        await client.post(
            f"/api/v1/sessions/{session_id}/complete",
            json={
                "outcome": "resolved",
                "outcome_notes": "Replaced failed DIMM in slot A2",
                "next_steps": "Monitor for 24 hours"
            },
            headers=auth_headers
        )

        # Test markdown
        response = await client.post(
            f"/api/v1/sessions/{session_id}/export",
            json={"format": "markdown"},
            headers=auth_headers
        )
        assert response.status_code == 200
        content = response.text
        assert "## Resolution" in content
        assert "Replaced failed DIMM in slot A2" in content
        assert "## Next Steps" in content
        assert "Monitor for 24 hours" in content

        # Test text
        response = await client.post(
            f"/api/v1/sessions/{session_id}/export",
            json={"format": "text"},
            headers=auth_headers
        )
        content = response.text
        assert "RESOLUTION" in content
        assert "Replaced failed DIMM in slot A2" in content
        assert "NEXT STEPS" in content
        assert "Monitor for 24 hours" in content

        # Test HTML
        response = await client.post(
            f"/api/v1/sessions/{session_id}/export",
            json={"format": "html"},
            headers=auth_headers
        )
        content = response.text
        assert "Resolution" in content
        assert "Replaced failed DIMM in slot A2" in content
        assert "Next Steps" in content
        assert "Monitor for 24 hours" in content

        # Test PSA
        response = await client.post(
            f"/api/v1/sessions/{session_id}/export",
            json={"format": "psa"},
            headers=auth_headers
        )
        content = response.text
        assert "Replaced failed DIMM in slot A2" in content
        assert "Monitor for 24 hours" in content

    @pytest.mark.asyncio
    async def test_export_omits_empty_resolution_and_next_steps(
        self, client: AsyncClient, auth_headers: dict, test_tree: dict
    ):
        """Test that empty outcome_notes/next_steps don't create empty sections."""
        create_response = await client.post(
            "/api/v1/sessions",
            json={"tree_id": test_tree["id"]},
            headers=auth_headers
        )
        session_id = create_response.json()["id"]

        await client.post(
            f"/api/v1/sessions/{session_id}/complete",
            json={"outcome": "resolved"},
            headers=auth_headers
        )

        response = await client.post(
            f"/api/v1/sessions/{session_id}/export",
            json={"format": "markdown"},
            headers=auth_headers
        )
        content = response.text
        assert "## Resolution" not in content
        assert "## Next Steps" not in content

    @pytest.mark.asyncio
    async def test_export_exclude_outcome_notes_flag(
        self, client: AsyncClient, auth_headers: dict, test_tree: dict
    ):
        """Test include_outcome_notes=False suppresses resolution section."""
        create_response = await client.post(
            "/api/v1/sessions",
            json={"tree_id": test_tree["id"]},
            headers=auth_headers
        )
        session_id = create_response.json()["id"]

        await client.post(
            f"/api/v1/sessions/{session_id}/complete",
            json={
                "outcome": "resolved",
                "outcome_notes": "Should not appear"
            },
            headers=auth_headers
        )

        response = await client.post(
            f"/api/v1/sessions/{session_id}/export",
            json={"format": "markdown", "include_outcome_notes": False},
            headers=auth_headers
        )
        content = response.text
        assert "## Resolution" not in content
        assert "Should not appear" not in content

    @pytest.mark.asyncio
    async def test_export_max_step_index(
        self, client: AsyncClient, auth_headers: dict, test_tree: dict
    ):
        """Test max_step_index limits exported steps."""
        create_response = await client.post(
            "/api/v1/sessions",
            json={"tree_id": test_tree["id"]},
            headers=auth_headers
        )
        session_id = create_response.json()["id"]

        decisions = [
            {"node_id": "n1", "question": "Step one?", "answer": "Yes", "timestamp": "2026-02-13T10:00:00Z", "attachments": []},
            {"node_id": "n2", "question": "Step two?", "answer": "No", "timestamp": "2026-02-13T10:01:00Z", "attachments": []},
            {"node_id": "n3", "question": "Step three?", "answer": "Maybe", "timestamp": "2026-02-13T10:02:00Z", "attachments": []},
        ]
        await client.put(
            f"/api/v1/sessions/{session_id}",
            json={"decisions": decisions},
            headers=auth_headers
        )

        response = await client.post(
            f"/api/v1/sessions/{session_id}/export",
            json={"format": "markdown", "max_step_index": 2},
            headers=auth_headers
        )
        content = response.text
        assert "Step one?" in content
        assert "Step two?" in content
        assert "Step three?" not in content

    @pytest.mark.asyncio
    async def test_export_max_step_index_exceeds_count(
        self, client: AsyncClient, auth_headers: dict, test_tree: dict
    ):
        """Test max_step_index larger than decision count returns all steps."""
        create_response = await client.post(
            "/api/v1/sessions",
            json={"tree_id": test_tree["id"]},
            headers=auth_headers
        )
        session_id = create_response.json()["id"]

        decisions = [
            {"node_id": "n1", "question": "Only step", "answer": "Done", "timestamp": "2026-02-13T10:00:00Z", "attachments": []},
        ]
        await client.put(
            f"/api/v1/sessions/{session_id}",
            json={"decisions": decisions},
            headers=auth_headers
        )

        response = await client.post(
            f"/api/v1/sessions/{session_id}/export",
            json={"format": "markdown", "max_step_index": 100},
            headers=auth_headers
        )
        assert response.status_code == 200
        assert "Only step" in response.text

    @pytest.mark.asyncio
    async def test_export_max_step_index_zero_returns_422(
        self, client: AsyncClient, auth_headers: dict, test_tree: dict
    ):
        """Test max_step_index=0 returns validation error."""
        create_response = await client.post(
            "/api/v1/sessions",
            json={"tree_id": test_tree["id"]},
            headers=auth_headers
        )
        session_id = create_response.json()["id"]

        response = await client.post(
            f"/api/v1/sessions/{session_id}/export",
            json={"format": "markdown", "max_step_index": 0},
            headers=auth_headers
        )
        assert response.status_code == 422

    @pytest.mark.asyncio
    async def test_export_in_progress_session_does_not_mark_exported(
        self, client: AsyncClient, auth_headers: dict, test_tree: dict
    ):
        """Test that exporting an in-progress session does NOT set exported=True."""
        create_response = await client.post(
            "/api/v1/sessions",
            json={"tree_id": test_tree["id"]},
            headers=auth_headers
        )
        session_id = create_response.json()["id"]

        await client.post(
            f"/api/v1/sessions/{session_id}/export",
            json={"format": "markdown"},
            headers=auth_headers
        )

        response = await client.get(
            f"/api/v1/sessions/{session_id}",
            headers=auth_headers
        )
        assert response.json()["exported"] is False

    @pytest.mark.asyncio
    async def test_export_completed_session_marks_exported(
        self, client: AsyncClient, auth_headers: dict, test_tree: dict
    ):
        """Test that exporting a completed session sets exported=True."""
        create_response = await client.post(
            "/api/v1/sessions",
            json={"tree_id": test_tree["id"]},
            headers=auth_headers
        )
        session_id = create_response.json()["id"]

        await client.post(
            f"/api/v1/sessions/{session_id}/complete",
            json={"outcome": "resolved"},
            headers=auth_headers
        )

        await client.post(
            f"/api/v1/sessions/{session_id}/export",
            json={"format": "markdown"},
            headers=auth_headers
        )

        response = await client.get(
            f"/api/v1/sessions/{session_id}",
            headers=auth_headers
        )
        assert response.json()["exported"] is True
