"""Tests for API endpoints."""

from unittest.mock import patch


class TestHealthEndpoint:
    """Tests for health check endpoint."""

    def test_health_check(self, client):
        """Test health check returns a valid response (503 if env vars missing, 200 if present)."""
        response = client.get("/health")
        assert response.status_code in (200, 503)
        data = response.json()
        assert data["status"] in ("healthy", "unhealthy")
        assert data["service"] == "hikaflow-debugger-api"


class TestAuthMiddleware:
    """Tests for authentication middleware."""

    def test_missing_auth_header(self, client):
        """Test request without auth header returns 401."""
        response = client.get("/v1/runs")
        assert response.status_code == 401
        assert "Missing bearer token" in response.json()["detail"]

    def test_invalid_auth_header_format(self, client):
        """Test request with invalid auth format returns 401."""
        response = client.get("/v1/runs", headers={"Authorization": "Basic abc123"})
        assert response.status_code == 401

    def test_invalid_token(self, client):
        """Test request with invalid token returns 401."""
        with patch("api.auth.verify_clerk_jwt") as mock_verify:
            mock_verify.side_effect = Exception("Invalid token")
            response = client.get("/v1/runs", headers={"Authorization": "Bearer invalid"})
            assert response.status_code == 401


class TestRunEndpoints:
    """Tests for run-related endpoints."""

    def test_get_run_success(self, client, auth_header, sample_run):
        """Test getting a run successfully."""
        with patch("api.db.get_run") as mock_get_run:
            mock_get_run.return_value = sample_run
            response = client.get(
                f"/v1/runs/{sample_run['id']}",
                headers=auth_header,
            )
            assert response.status_code == 200
            data = response.json()
            assert data["run_id"] == sample_run["id"]
            assert data["status"] == sample_run["status"]

    def test_get_run_not_found(self, client, auth_header):
        """Test getting non-existent run returns 404."""
        with patch("api.db.get_run") as mock_get_run:
            mock_get_run.return_value = None
            response = client.get(
                "/v1/runs/550e8400-e29b-41d4-a716-446655440000",
                headers=auth_header,
            )
            assert response.status_code == 404
            assert "not found" in response.json()["detail"].lower()

    def test_get_run_access_denied(self, client, auth_header, sample_run):
        """Test accessing run from different org returns 404."""
        sample_run["org_id"] = "different_org"
        with patch("api.db.get_run") as mock_get_run:
            mock_get_run.return_value = sample_run
            response = client.get(
                f"/v1/runs/{sample_run['id']}",
                headers=auth_header,
            )
            assert response.status_code == 404

    def test_get_run_invalid_uuid(self, client, auth_header):
        """Test getting run with invalid UUID returns 400."""
        response = client.get(
            "/v1/runs/not-a-valid-uuid",
            headers=auth_header,
        )
        assert response.status_code == 400

    def test_list_runs_success(self, client, auth_header, sample_run):
        """Test listing runs successfully."""
        with patch("api.db.list_runs") as mock_list:
            mock_list.return_value = {
                "runs": [sample_run],
                "total": 1,
            }
            response = client.get("/v1/runs", headers=auth_header)
            assert response.status_code == 200
            data = response.json()
            assert len(data["runs"]) == 1
            assert data["total"] == 1

    def test_list_runs_with_filters(self, client, auth_header):
        """Test listing runs with filters."""
        with patch("api.db.list_runs") as mock_list:
            mock_list.return_value = {"runs": [], "total": 0}
            response = client.get(
                "/v1/runs?status=FAILED&page=2&per_page=10",
                headers=auth_header,
            )
            assert response.status_code == 200
            mock_list.assert_called_once()
            call_kwargs = mock_list.call_args[1]
            assert call_kwargs["status"] == "FAILED"
            assert call_kwargs["page"] == 2
            assert call_kwargs["per_page"] == 10

    def test_list_runs_invalid_status(self, client, auth_header):
        """Test listing runs with invalid status returns 400."""
        response = client.get(
            "/v1/runs?status=INVALID",
            headers=auth_header,
        )
        assert response.status_code == 400

    def test_finalize_run_success(self, client, auth_header, sample_run):
        """Test finalizing a run successfully."""
        with patch("api.db.get_run") as mock_get, \
             patch("api.db.finalize_run") as mock_finalize:
            mock_get.return_value = sample_run
            response = client.post(
                f"/v1/runs/{sample_run['id']}/finalize",
                headers=auth_header,
                json={"duration": 1.5, "exit_code": 0, "io_count": 10},
            )
            assert response.status_code == 200
            assert response.json()["status"] == "ok"
            mock_finalize.assert_called_once()

    def test_get_artifact_success(self, client, auth_header, sample_run):
        """Test getting artifact download URL."""
        with patch("api.db.get_run") as mock_get, \
             patch("api.storage.sign_download_url") as mock_sign:
            mock_get.return_value = sample_run
            mock_sign.return_value = "https://example.com/artifact"
            response = client.get(
                f"/v1/runs/{sample_run['id']}/artifacts/recording.json",
                headers=auth_header,
            )
            assert response.status_code == 200
            assert "url" in response.json()

    def test_get_artifact_path_traversal(self, client, auth_header, sample_run):
        """Test artifact path traversal is blocked.

        Note: The HTTP client/framework normalizes path traversal sequences,
        so ../../../etc/passwd becomes /v1/etc/passwd which returns 404.
        This effectively blocks the attack - the user cannot access files
        outside the intended path.
        """
        with patch("api.db.get_run") as mock_get:
            mock_get.return_value = sample_run
            response = client.get(
                f"/v1/runs/{sample_run['id']}/artifacts/../../../etc/passwd",
                headers=auth_header,
            )
            # Path traversal is blocked - either by explicit 400 validation
            # or by framework normalizing the path resulting in 404
            assert response.status_code in (400, 404)


class TestProjectEndpoints:
    """Tests for project-related endpoints."""

    def test_list_projects_success(self, client, auth_header, sample_project):
        """Test listing projects successfully."""
        with patch("api.db.list_projects") as mock_list:
            mock_list.return_value = [sample_project]
            response = client.get("/v1/projects", headers=auth_header)
            assert response.status_code == 200
            data = response.json()
            assert len(data) == 1
            assert data[0]["name"] == sample_project["name"]

    def test_create_project_success(self, client, auth_header):
        """Test creating a project successfully."""
        with patch("api.db.create_project") as mock_create:
            response = client.post(
                "/v1/projects",
                headers=auth_header,
                json={
                    "name": "New Project",
                    "description": "A new project",
                    "repo": "myorg/myrepo",
                },
            )
            assert response.status_code == 200
            data = response.json()
            assert data["name"] == "New Project"
            mock_create.assert_called_once()

    def test_create_project_invalid_name(self, client, auth_header):
        """Test creating project with invalid name returns 400."""
        response = client.post(
            "/v1/projects",
            headers=auth_header,
            json={
                "name": "",  # Empty name
            },
        )
        assert response.status_code == 400

    def test_get_project_success(self, client, auth_header, sample_project):
        """Test getting a project successfully."""
        with patch("api.db.get_project") as mock_get:
            mock_get.return_value = sample_project
            response = client.get(
                f"/v1/projects/{sample_project['id']}",
                headers=auth_header,
            )
            assert response.status_code == 200
            data = response.json()
            assert data["name"] == sample_project["name"]

    def test_get_project_access_denied(self, client, auth_header, sample_project):
        """Test accessing project from different org returns 404."""
        sample_project["org_id"] = "different_org"
        with patch("api.db.get_project") as mock_get:
            mock_get.return_value = sample_project
            response = client.get(
                f"/v1/projects/{sample_project['id']}",
                headers=auth_header,
            )
            assert response.status_code == 404

    def test_delete_project_success(self, client, auth_header, sample_project):
        """Test deleting a project successfully."""
        with patch("api.db.get_project") as mock_get, \
             patch("api.db.delete_project") as mock_delete:
            mock_get.return_value = sample_project
            response = client.delete(
                f"/v1/projects/{sample_project['id']}",
                headers=auth_header,
            )
            assert response.status_code == 200
            mock_delete.assert_called_once()


class TestFixEndpoints:
    """Tests for fix-related endpoints."""

    def test_get_run_fixes_success(self, client, auth_header, sample_run, sample_fix):
        """Test getting fixes for a run."""
        with patch("api.db.get_run") as mock_get_run, \
             patch("api.db.get_run_fixes") as mock_get_fixes:
            mock_get_run.return_value = sample_run
            mock_get_fixes.return_value = [sample_fix]
            response = client.get(
                f"/v1/runs/{sample_run['id']}/fixes",
                headers=auth_header,
            )
            assert response.status_code == 200
            data = response.json()
            assert len(data) == 1
            assert data[0]["root_cause"] == sample_fix["root_cause"]

    def test_apply_fix_success(self, client, auth_header, sample_run, sample_fix):
        """Test applying a fix successfully."""
        with patch("api.db.get_fix") as mock_get_fix, \
             patch("api.db.get_run") as mock_get_run, \
             patch("api.db.update_fix_status") as mock_update:
            mock_get_fix.return_value = sample_fix
            mock_get_run.return_value = sample_run
            response = client.post(
                f"/v1/fixes/{sample_fix['id']}/apply",
                headers=auth_header,
            )
            assert response.status_code == 200
            mock_update.assert_called_once_with(sample_fix["id"], "APPLIED")

    def test_reject_fix_success(self, client, auth_header, sample_run, sample_fix):
        """Test rejecting a fix successfully."""
        with patch("api.db.get_fix") as mock_get_fix, \
             patch("api.db.get_run") as mock_get_run, \
             patch("api.db.update_fix_status") as mock_update:
            mock_get_fix.return_value = sample_fix
            mock_get_run.return_value = sample_run
            response = client.post(
                f"/v1/fixes/{sample_fix['id']}/reject",
                headers=auth_header,
            )
            assert response.status_code == 200
            mock_update.assert_called_once_with(sample_fix["id"], "REJECTED")

    def test_trigger_auto_debug_success(self, client, auth_header, sample_run):
        """Test triggering auto-debug successfully."""
        with patch("api.db.get_run") as mock_get, \
             patch("api.queue.enqueue_auto_debug") as mock_queue:
            mock_get.return_value = sample_run
            mock_queue.return_value = "job_123"
            response = client.post(
                f"/v1/runs/{sample_run['id']}/auto-debug",
                headers=auth_header,
            )
            assert response.status_code == 200
            data = response.json()
            assert data["job_id"] == "job_123"
            assert data["status"] == "queued"

    def test_create_fix_pr_success(
        self, client, auth_header, sample_run, sample_fix, sample_installation
    ):
        """Test creating a PR for a fix."""
        with patch("api.db.get_fix") as mock_get_fix, \
             patch("api.db.get_run") as mock_get_run, \
             patch("api.db.get_installation_by_repo") as mock_get_install, \
             patch("api.db.update_fix_status") as mock_update, \
             patch("api.db.save_fix_pr") as mock_save_pr, \
             patch("api.github_app.create_fix_pr") as mock_create_pr:
            mock_get_fix.return_value = sample_fix
            mock_get_run.return_value = sample_run
            mock_get_install.return_value = sample_installation
            mock_create_pr.return_value = {
                "pr_url": "https://github.com/testorg/testrepo/pull/1",
                "pr_number": 1,
                "branch": "hikaflow/fix-abc123",
            }

            response = client.post(
                f"/v1/fixes/{sample_fix['id']}/create-pr",
                headers=auth_header,
                json={
                    "fix_id": sample_fix["id"],
                    "owner": "testorg",
                    "repo": "testrepo",
                },
            )
            assert response.status_code == 200
            data = response.json()
            assert data["pr_number"] == 1
            assert "pr_url" in data

    def test_create_fix_pr_invalid_repo(self, client, auth_header, sample_run, sample_fix):
        """Test creating PR with invalid repo name returns 400."""
        with patch("api.db.get_fix") as mock_get_fix, \
             patch("api.db.get_run") as mock_get_run:
            mock_get_fix.return_value = sample_fix
            mock_get_run.return_value = sample_run

            response = client.post(
                f"/v1/fixes/{sample_fix['id']}/create-pr",
                headers=auth_header,
                json={
                    "fix_id": sample_fix["id"],
                    "owner": "test..org",  # Invalid
                    "repo": "test/repo",  # Invalid
                },
            )
            assert response.status_code == 400

    def test_create_fix_pr_no_installation(
        self, client, auth_header, sample_run, sample_fix
    ):
        """Test creating PR without GitHub installation returns 400."""
        with patch("api.db.get_fix") as mock_get_fix, \
             patch("api.db.get_run") as mock_get_run, \
             patch("api.db.get_installation_by_repo") as mock_get_install:
            mock_get_fix.return_value = sample_fix
            mock_get_run.return_value = sample_run
            mock_get_install.return_value = None

            response = client.post(
                f"/v1/fixes/{sample_fix['id']}/create-pr",
                headers=auth_header,
                json={
                    "fix_id": sample_fix["id"],
                    "owner": "testorg",
                    "repo": "testrepo",
                },
            )
            assert response.status_code == 400
            assert "not installed" in response.json()["detail"].lower()


class TestCorrelationId:
    """Tests for correlation ID middleware."""

    def test_correlation_id_generated(self, client, auth_header, sample_run):
        """Test correlation ID is generated and returned in response."""
        with patch("api.db.get_run") as mock_get:
            mock_get.return_value = sample_run
            response = client.get(
                f"/v1/runs/{sample_run['id']}",
                headers=auth_header,
            )
            assert "X-Correlation-ID" in response.headers
            assert len(response.headers["X-Correlation-ID"]) > 0

    def test_correlation_id_passed_through(self, client, auth_header, sample_run):
        """Test correlation ID from request is used in response."""
        with patch("api.db.get_run") as mock_get:
            mock_get.return_value = sample_run
            headers = {**auth_header, "X-Correlation-ID": "test-correlation-123"}
            response = client.get(
                f"/v1/runs/{sample_run['id']}",
                headers=headers,
            )
            assert response.headers["X-Correlation-ID"] == "test-correlation-123"


class TestInputValidation:
    """Tests for input validation."""

    def test_uuid_validation(self, client, auth_header):
        """Test invalid UUIDs are rejected."""
        invalid_uuids = [
            "not-a-uuid",
            "123",
            "550e8400-e29b-41d4-a716",  # Too short
            "550e8400-e29b-41d4-a716-446655440000-extra",  # Too long
        ]
        for invalid_uuid in invalid_uuids:
            response = client.get(
                f"/v1/runs/{invalid_uuid}",
                headers=auth_header,
            )
            assert response.status_code == 400

    def test_pagination_validation(self, client, auth_header):
        """Test pagination parameters are validated."""
        with patch("api.db.list_runs") as mock_list:
            mock_list.return_value = {"runs": [], "total": 0}

            # Test page < 1 is rejected
            response = client.get(
                "/v1/runs?page=0",
                headers=auth_header,
            )
            assert response.status_code == 422  # Pydantic validation

            # Test per_page > 100 is capped
            response = client.get(
                "/v1/runs?per_page=200",
                headers=auth_header,
            )
            assert response.status_code == 422  # Pydantic validation


class TestSessionEndpoints:
    """Tests for session-related endpoints."""

    def test_get_session_success(self, client, auth_header, sample_session):
        """Test getting a session by ID."""
        with patch("api.db.get_session") as mock_get:
            mock_get.return_value = sample_session
            response = client.get(
                f"/v1/sessions/{sample_session['id']}",
                headers=auth_header,
            )
            assert response.status_code == 200
            data = response.json()
            assert data["session"]["id"] == sample_session["id"]
            assert data["session"]["status"] == "FINALIZED"

    def test_get_session_not_found(self, client, auth_header):
        """Test getting non-existent session returns 404."""
        with patch("api.db.get_session") as mock_get:
            mock_get.return_value = None
            response = client.get(
                "/v1/sessions/880e8400-e29b-41d4-a716-446655440000",
                headers=auth_header,
            )
            assert response.status_code == 404

    def test_get_session_access_denied(self, client, auth_header, sample_session):
        """Test accessing session from different org returns 404."""
        sample_session["org_id"] = "different_org"
        with patch("api.db.get_session") as mock_get:
            mock_get.return_value = sample_session
            response = client.get(
                f"/v1/sessions/{sample_session['id']}",
                headers=auth_header,
            )
            assert response.status_code == 404

    def test_get_session_invalid_uuid(self, client, auth_header):
        """Test getting session with invalid UUID returns 400."""
        response = client.get(
            "/v1/sessions/not-a-valid-uuid",
            headers=auth_header,
        )
        assert response.status_code == 400

    def test_list_sessions_success(self, client, auth_header, sample_session):
        """Test listing sessions successfully."""
        with patch("api.db.list_sessions") as mock_list:
            mock_list.return_value = {
                "sessions": [sample_session],
                "total": 1,
            }
            response = client.get("/v1/sessions", headers=auth_header)
            assert response.status_code == 200
            data = response.json()
            assert len(data["sessions"]) == 1
            assert data["total"] == 1

    def test_delete_session_success(self, client, auth_header, sample_session):
        """Test deleting a session successfully."""
        with patch("api.db.get_session") as mock_get, \
             patch("api.db.list_session_chunks") as mock_chunks, \
             patch("api.db.delete_session") as mock_delete, \
             patch("api.storage.delete_session_object"):
            mock_get.return_value = sample_session
            mock_chunks.return_value = []
            response = client.delete(
                f"/v1/sessions/{sample_session['id']}",
                headers=auth_header,
            )
            assert response.status_code == 200
            assert response.json()["status"] == "deleted"
            mock_delete.assert_called_once_with(sample_session["id"])

    def test_list_session_chunks_success(self, client, auth_header, sample_session):
        """Test listing session chunks."""
        chunks = [
            {"chunk_id": "c1", "stream": "dom", "storage_key": "s1/dom/c1.jsonl.zst", "byte_size": 100, "event_count": 10},
        ]
        with patch("api.db.get_session") as mock_get, \
             patch("api.db.list_session_chunks") as mock_chunks, \
             patch("api.storage.sign_download_url") as mock_sign:
            mock_get.return_value = sample_session
            mock_chunks.return_value = chunks
            mock_sign.return_value = "https://example.com/download"
            response = client.get(
                f"/v1/sessions/{sample_session['id']}/chunks",
                headers=auth_header,
            )
            assert response.status_code == 200
            data = response.json()
            assert len(data["chunks"]) == 1
            assert "download_url" in data["chunks"][0]

    def test_list_session_chunks_quarantined(self, client, auth_header, sample_session):
        """Test quarantined session chunks are blocked."""
        sample_session["status"] = "QUARANTINED"
        with patch("api.db.get_session") as mock_get:
            mock_get.return_value = sample_session
            response = client.get(
                f"/v1/sessions/{sample_session['id']}/chunks",
                headers=auth_header,
            )
            assert response.status_code == 403

    def test_list_sessions_with_status_filter(self, client, auth_header, sample_session):
        """Test listing sessions filtered by status."""
        with patch("api.db.list_sessions") as mock_list:
            mock_list.return_value = {"sessions": [sample_session], "total": 1}
            response = client.get("/v1/sessions?status=FINALIZED", headers=auth_header)
            assert response.status_code == 200
            mock_list.assert_called_once()
            call_kwargs = mock_list.call_args[1]
            assert call_kwargs["status"] == "FINALIZED"

    def test_list_sessions_with_search(self, client, auth_header, sample_session):
        """Test listing sessions with search query."""
        with patch("api.db.list_sessions") as mock_list:
            mock_list.return_value = {"sessions": [sample_session], "total": 1}
            response = client.get("/v1/sessions?search=880e", headers=auth_header)
            assert response.status_code == 200
            mock_list.assert_called_once()
            call_kwargs = mock_list.call_args[1]
            assert call_kwargs["search"] == "880e"

    def test_list_sessions_invalid_status(self, client, auth_header):
        """Test listing sessions with invalid status returns 400."""
        response = client.get("/v1/sessions?status=INVALID", headers=auth_header)
        assert response.status_code == 400

    def test_session_stats(self, client, auth_header):
        """Test session stats endpoint."""
        with patch("api.db.get_session_stats") as mock_stats:
            mock_stats.return_value = {"total": 5, "finalized": 3}
            response = client.get("/v1/sessions/stats", headers=auth_header)
            assert response.status_code == 200
            data = response.json()
            assert data["counts"]["total"] == 5
