# #region ValidationApiTests [C:3] [TYPE Module] [SEMANTICS validation, api, tests, pagination, crud]
# @BRIEF Unit tests for validation task CRUD and run history API endpoints.
# @LAYER API
# @RELATION BINDS_TO -> [ValidationRoutes]
# @RELATION DEPENDS_ON -> [EXT:FastAPI:TestClient]
# @TEST_CONTRACT  [ValidationTaskCreate|Update|RunRequest] -> [ValidationTaskResponse|RunResponse|TriggerRunResponse]
# @TEST_SCENARIO  list_tasks -> 200 with paginated tasks; filter by is_active/environment_id
# @TEST_SCENARIO  create_task -> 201 with valid payload; 422 with invalid provider/environment
# @TEST_SCENARIO  get_task -> 200 with task+recent_runs; 404 for nonexistent
# @TEST_SCENARIO  update_task -> 200 with updated fields; 404 for nonexistent
# @TEST_SCENARIO  delete_task -> 204 on success; 404 for nonexistent
# @TEST_SCENARIO  trigger_run -> 200 with spawned_task_id; 422 for invalid task
# @TEST_SCENARIO  list_runs -> 200 with 6 filters + pagination
# @TEST_SCENARIO  get_run_detail -> 200 with issues/raw_response; 404 for nonexistent
# @TEST_SCENARIO  delete_run -> 204 on success; 404 for nonexistent
# @TEST_EDGE  missing_field -> 422 when required field omitted from create payload
# @TEST_EDGE  invalid_type -> 422 when provider_id is not multimodal
# @TEST_EDGE  external_fail -> 404/422 when task/run does not exist
# @TEST_EDGE  delete_task_with_runs -> 204 with delete_runs=true flag
# @INVARIANT  Every endpoint requires authentication via has_permission
# @INVARIANT  All list endpoints support pagination (page/page_size) with defaults
# @INVARIANT  provider_id must reference a multimodal LLM provider for creation

# Set required env vars before ANY app imports — crash-early guard for AuthConfig()
import os

os.environ.setdefault("SECRET_KEY", "test-secret-key-for-testing")
os.environ.setdefault("AUTH_DATABASE_URL", "sqlite:///:memory:test_auth")
os.environ.setdefault("DATABASE_URL", "sqlite:///:memory:test_main")
os.environ.setdefault("DEV_MODE", "true")

from datetime import UTC, datetime
import pytest
from unittest.mock import AsyncMock, MagicMock

from fastapi.testclient import TestClient

from src.api.routes.validation import _get_run_service, _get_task_service
from src.app import app
from src.dependencies import (
    get_config_manager,
    get_current_user,
    get_task_manager,
)
from src.models.auth import User
from src.schemas.validation import (
    TriggerRunResponse,
    ValidationRunDetailResponse,
    ValidationRunListResponse,
    ValidationTaskListResponse,
    ValidationTaskResponse,
)

# ── Shared mocks ───────────────────────────────────────────────────────

mock_user = MagicMock(spec=User)
mock_user.username = "testuser"
mock_user.roles = []
admin_role = MagicMock()
admin_role.name = "Admin"
admin_role.permissions = []
mock_user.roles.append(admin_role)


def _make_now():
    """Return a timezone-aware UTC datetime for Pydantic v2."""
    return datetime.now(UTC)


@pytest.fixture(autouse=True)
def mock_all_deps():
    """Override service-level FastAPI dependencies with MagicMock stubs.

    Permission checking (has_permission) is NOT overridden — mock_user has
    Admin role which passes all permission gates implicitly.
    Fixture resets mock_user.roles before each test to prevent cross-test pollution.
    """
    # Reset user state — Admin role passes all permission gates
    mock_user.roles = [admin_role]

    mock_task_service = MagicMock()
    mock_run_service = MagicMock()
    mock_task_manager = MagicMock()

    app.dependency_overrides[_get_task_service] = lambda: mock_task_service
    app.dependency_overrides[_get_run_service] = lambda: mock_run_service
    app.dependency_overrides[get_config_manager] = lambda: MagicMock()
    app.dependency_overrides[get_task_manager] = lambda: mock_task_manager
    app.dependency_overrides[get_current_user] = lambda: mock_user

    yield {
        "task_service": mock_task_service,
        "run_service": mock_run_service,
        "task_manager": mock_task_manager,
    }
    app.dependency_overrides.clear()


client = TestClient(app)


# ── Fixtures ────────────────────────────────────────────────────────────

def _make_task_response(**overrides):
    """Build a ValidationTaskResponse for mock returns."""
    return ValidationTaskResponse(
        id=overrides.get("id", "task-1"),
        name=overrides.get("name", "Test Task"),
        environment_id=overrides.get("environment_id", "env-1"),
        is_active=overrides.get("is_active", True),
        dashboard_ids=overrides.get("dashboard_ids", ["dash-1"]),
        provider_id=overrides.get("provider_id", "provider-1"),
        schedule_days=None,
        window_start=None,
        window_end=None,
        notify_owners=True,
        custom_channels=None,
        alert_condition="FAIL_ONLY",
        created_at=overrides.get("created_at", _make_now()),
        updated_at=None,
        last_run_status=None,
        last_run_at=None,
    )


def _make_run_response(**overrides):
    """Build a dict representation of a ValidationRunResponse."""
    return {
        "id": overrides.get("id", "run-1"),
        "task_id": overrides.get("task_id", "task-1"),
        "dashboard_id": overrides.get("dashboard_id", "dash-1"),
        "environment_id": overrides.get("environment_id", "env-1"),
        "status": overrides.get("status", "PASS"),
        "summary": overrides.get("summary", ""),
        "timestamp": overrides.get("timestamp", _make_now()),
        "screenshot_path": None,
        "task_name": overrides.get("task_name", "Test Task"),
    }


# ── Task CRUD Tests ────────────────────────────────────────────────────

# #region test_list_tasks_success [C:2] [TYPE Function]
# @BRIEF GET /validation/tasks returns paginated task list with filters.
def test_list_tasks_success(mock_all_deps):
    task = _make_task_response()
    mock_all_deps["task_service"].list_tasks.return_value = (1, [task])

    response = client.get("/api/validation/tasks")
    assert response.status_code == 200
    data = response.json()
    assert data["items"][0]["name"] == "Test Task"
    assert data["total"] == 1
    assert data["page"] == 1
    assert data["page_size"] == 20
    ValidationTaskListResponse(**data)

    # environment_id filter
    response = client.get("/api/validation/tasks?environment_id=env-1")
    assert response.status_code == 200
    mock_all_deps["task_service"].list_tasks.assert_called_with(
        is_active=None, environment_id="env-1", page=1, page_size=20
    )
# #endregion test_list_tasks_success


# #region test_list_tasks_pagination [C:2] [TYPE Function]
# @BRIEF Pagination query params are validated: page >= 1, page_size 1-100.
def test_list_tasks_pagination(mock_all_deps):
    response = client.get("/api/validation/tasks?page=0")
    assert response.status_code == 422

    response = client.get("/api/validation/tasks?page_size=101")
    assert response.status_code == 422

    response = client.get("/api/validation/tasks?page_size=0")
    assert response.status_code == 422
# #endregion test_list_tasks_pagination


# #region test_list_tasks_service_error [C:2] [TYPE Function]
# @BRIEF Service ValueError becomes 400 on list_tasks.
def test_list_tasks_service_error(mock_all_deps):
    mock_all_deps["task_service"].list_tasks.side_effect = ValueError("Bad filter")
    response = client.get("/api/validation/tasks?environment_id=nonexistent")
    assert response.status_code == 400
    assert "Bad filter" in response.json()["detail"]
# #endregion test_list_tasks_service_error


# #region test_create_task_success [C:2] [TYPE Function]
# @BRIEF POST /validation/tasks with valid payload returns 201.
def test_create_task_success(mock_all_deps):
    mock_all_deps["task_service"].create_task.return_value = _make_task_response()

    payload = {
        "name": "My Validation Task",
        "environment_id": "env-1",
        "dashboard_ids": ["dash-1"],
        "provider_id": "provider-1",
        "notify_owners": True,
        "alert_condition": "FAIL_ONLY",
    }
    response = client.post("/api/validation/tasks", json=payload)
    assert response.status_code == 201
    data = response.json()
    assert data["name"] == "Test Task"
    assert data["id"] == "task-1"
    ValidationTaskResponse(**data)
    mock_all_deps["task_service"].create_task.assert_called_once()
# #endregion test_create_task_success


# #region test_create_task_missing_fields [C:2] [TYPE Function]
# @BRIEF POST with missing required fields returns 422.
def test_create_task_missing_fields(mock_all_deps):
    # Empty payload
    response = client.post("/api/validation/tasks", json={})
    assert response.status_code == 422
    errors = response.json()
    assert "detail" in errors

    # Missing provider_id
    response = client.post("/api/validation/tasks", json={
        "name": "Test", "environment_id": "env-1", "dashboard_ids": ["dash-1"],
    })
    assert response.status_code == 422

    # Missing dashboard_ids
    response = client.post("/api/validation/tasks", json={
        "name": "Test", "environment_id": "env-1", "provider_id": "provider-1",
    })
    assert response.status_code == 422
# #endregion test_create_task_missing_fields


# #region test_create_task_invalid_provider [C:2] [TYPE Function]
# @BRIEF POST with non-multimodal provider returns 422.
def test_create_task_invalid_provider(mock_all_deps):
    mock_all_deps["task_service"].create_task.side_effect = ValueError(
        "LLM provider 'GPT-3.5' is not multimodal"
    )
    payload = {
        "name": "Test", "environment_id": "env-1",
        "dashboard_ids": ["dash-1"], "provider_id": "provider-2",
    }
    response = client.post("/api/validation/tasks", json=payload)
    assert response.status_code == 422
    assert "not multimodal" in response.json()["detail"]
# #endregion test_create_task_invalid_provider


# #region test_get_task_success [C:2] [TYPE Function]
# @BRIEF GET /validation/tasks/{id} returns task with recent_runs.
def test_get_task_success(mock_all_deps):
    mock_all_deps["task_service"].get_task.return_value = _make_task_response()
    mock_all_deps["run_service"].list_runs.return_value = (0, [])

    response = client.get("/api/validation/tasks/task-1")
    assert response.status_code == 200
    data = response.json()
    assert "task" in data
    assert "recent_runs" in data
    assert data["task"]["name"] == "Test Task"
    mock_all_deps["task_service"].get_task.assert_called_with("task-1")
# #endregion test_get_task_success


# #region test_get_task_not_found [C:2] [TYPE Function]
# @BRIEF GET for nonexistent task returns 404.
def test_get_task_not_found(mock_all_deps):
    mock_all_deps["task_service"].get_task.side_effect = ValueError("not found")
    response = client.get("/api/validation/tasks/nonexistent")
    assert response.status_code == 404
# #endregion test_get_task_not_found


# #region test_update_task_success [C:2] [TYPE Function]
# @BRIEF PUT /validation/tasks/{id} updates task and returns updated object.
def test_update_task_success(mock_all_deps):
    updated = _make_task_response(name="Updated Name", is_active=False)
    mock_all_deps["task_service"].update_task.return_value = updated

    payload = {"name": "Updated Name", "is_active": False}
    response = client.put("/api/validation/tasks/task-1", json=payload)
    assert response.status_code == 200
    data = response.json()
    assert data["name"] == "Updated Name"
    assert data["is_active"] is False
    ValidationTaskResponse(**data)
    mock_all_deps["task_service"].update_task.assert_called_once()
# #endregion test_update_task_success


# #region test_update_task_not_found [C:2] [TYPE Function]
# @BRIEF PUT for nonexistent task returns 404.
def test_update_task_not_found(mock_all_deps):
    mock_all_deps["task_service"].update_task.side_effect = ValueError("not found")
    payload = {"name": "Nope"}
    response = client.put("/api/validation/tasks/nonexistent", json=payload)
    assert response.status_code == 404
# #endregion test_update_task_not_found


# #region test_delete_task_success [C:2] [TYPE Function]
# @BRIEF DELETE /validation/tasks/{id} returns 204.
def test_delete_task_success(mock_all_deps):
    mock_all_deps["task_service"].delete_task.return_value = None
    response = client.delete("/api/validation/tasks/task-1")
    assert response.status_code == 204
    mock_all_deps["task_service"].delete_task.assert_called_with("task-1", delete_runs=False)
# #endregion test_delete_task_success


# #region test_delete_task_not_found [C:2] [TYPE Function]
# @BRIEF DELETE for nonexistent task returns 404.
def test_delete_task_not_found(mock_all_deps):
    mock_all_deps["task_service"].delete_task.side_effect = ValueError("not found")
    response = client.delete("/api/validation/tasks/nonexistent")
    assert response.status_code == 404
# #endregion test_delete_task_not_found


# #region test_delete_task_with_runs_flag [C:2] [TYPE Function]
# @BRIEF DELETE with delete_runs=true passes query param to service.
def test_delete_task_with_runs_flag(mock_all_deps):
    mock_all_deps["task_service"].delete_task.return_value = None
    response = client.delete("/api/validation/tasks/task-1?delete_runs=true")
    assert response.status_code == 204
    mock_all_deps["task_service"].delete_task.assert_called_with("task-1", delete_runs=True)
# #endregion test_delete_task_with_runs_flag


# #region test_trigger_run_success [C:2] [TYPE Function]
# @BRIEF POST /validation/tasks/{id}/run spawns a validation task.
def test_trigger_run_success(mock_all_deps):
    mock_all_deps["task_service"].trigger_run = AsyncMock(return_value="spawned-1")
    mock_all_deps["task_manager"].create_task = AsyncMock()

    response = client.post("/api/validation/tasks/task-1/run")
    assert response.status_code == 200
    data = response.json()
    assert data["task_id"] == "task-1"
    assert data["spawned_task_id"] == "spawned-1"
    assert data["status"] == "PENDING"
    TriggerRunResponse(**data)
# #endregion test_trigger_run_success


# #region test_trigger_run_not_found [C:2] [TYPE Function]
# @BRIEF POST for nonexistent task returns 422.
def test_trigger_run_not_found(mock_all_deps):
    mock_all_deps["task_service"].trigger_run = AsyncMock(
        side_effect=ValueError("Validation task 'nonexistent' not found")
    )
    response = client.post("/api/validation/tasks/nonexistent/run")
    assert response.status_code == 422
    assert "not found" in response.json()["detail"]
# #endregion test_trigger_run_not_found


# #region test_trigger_run_no_dashboards [C:2] [TYPE Function]
# @BRIEF Trigger fails with 422 when task has no dashboard IDs.
def test_trigger_run_no_dashboards(mock_all_deps):
    mock_all_deps["task_service"].trigger_run = AsyncMock(
        side_effect=ValueError("has no dashboard IDs configured")
    )
    response = client.post("/api/validation/tasks/task-1/run")
    assert response.status_code == 422
    assert "no dashboard IDs" in response.json()["detail"]
# #endregion test_trigger_run_no_dashboards


# ── Run History Tests ──────────────────────────────────────────────────

# #region test_list_runs_success [C:2] [TYPE Function]
# @BRIEF GET /validation/runs returns paginated run list.
def test_list_runs_success(mock_all_deps):
    run = _make_run_response()
    mock_all_deps["run_service"].list_runs.return_value = (1, [run])

    response = client.get("/api/validation/runs")
    assert response.status_code == 200
    data = response.json()
    assert data["items"][0]["id"] == "run-1"
    assert data["total"] == 1
    ValidationRunListResponse(**data)
# #endregion test_list_runs_success


# #region test_list_runs_filters [C:2] [TYPE Function]
# @BRIEF All 6 filter query params are accepted and forwarded to service.
def test_list_runs_filters(mock_all_deps):
    mock_all_deps["run_service"].list_runs.return_value = (0, [])

    for query in [
        "?task_id=task-1",
        "?dashboard_id=dash-1",
        "?environment_id=env-1",
        "?status=PASS",
        "?date_from=2026-01-01",
        "?date_to=2026-12-31",
        "?status=PASS&dashboard_id=dash-1&date_from=2026-01-01",
    ]:
        response = client.get(f"/api/validation/runs{query}")
        assert response.status_code == 200, f"Filter query failed: {query}"
# #endregion test_list_runs_filters


# #region test_list_runs_pagination [C:2] [TYPE Function]
# @BRIEF Pagination boundary checks on runs list.
def test_list_runs_pagination(mock_all_deps):
    response = client.get("/api/validation/runs?page=0")
    assert response.status_code == 422

    response = client.get("/api/validation/runs?page_size=101")
    assert response.status_code == 422
# #endregion test_list_runs_pagination


# #region test_list_runs_invalid_status [C:2] [TYPE Function]
# @BRIEF Status is a free string; any value passes through to service.
def test_list_runs_invalid_status(mock_all_deps):
    mock_all_deps["run_service"].list_runs.return_value = (0, [])
    response = client.get("/api/validation/runs?status=INVALID_STATUS_XYZ")
    assert response.status_code == 200
    call_kwargs = mock_all_deps["run_service"].list_runs.call_args[1]
    assert call_kwargs.get("status") == "INVALID_STATUS_XYZ"
# #endregion test_list_runs_invalid_status


# #region test_get_run_detail_success [C:2] [TYPE Function]
# @BRIEF GET /validation/runs/{id} returns full detail with issues and raw_response.
def test_get_run_detail_success(mock_all_deps):
    mock_all_deps["run_service"].get_run_detail.return_value = ValidationRunDetailResponse(
        id="run-1",
        task_id="task-1",
        dashboard_id="dash-1",
        environment_id="env-1",
        status="FAIL",
        summary="Charts failed to render",
        timestamp=_make_now(),
        screenshot_path="/tmp/screenshots/run-1.png",
        issues=[{"severity": "HIGH", "message": "Chart timeout"}],
        raw_response='{"status": "error"}',
        task_name="Test Task",
    )

    response = client.get("/api/validation/runs/run-1")
    assert response.status_code == 200
    data = response.json()
    assert data["status"] == "FAIL"
    assert len(data["issues"]) == 1
    assert data["raw_response"] is not None
    ValidationRunDetailResponse(**data)
# #endregion test_get_run_detail_success


# #region test_get_run_detail_not_found [C:2] [TYPE Function]
# @BRIEF GET for nonexistent run returns 404.
def test_get_run_detail_not_found(mock_all_deps):
    mock_all_deps["run_service"].get_run_detail.side_effect = ValueError("not found")
    response = client.get("/api/validation/runs/nonexistent")
    assert response.status_code == 404
# #endregion test_get_run_detail_not_found


# #region test_delete_run_success [C:2] [TYPE Function]
# @BRIEF DELETE /validation/runs/{id} returns 204.
def test_delete_run_success(mock_all_deps):
    mock_all_deps["run_service"].delete_run.return_value = True
    response = client.delete("/api/validation/runs/run-1")
    assert response.status_code == 204
    mock_all_deps["run_service"].delete_run.assert_called_with("run-1")
# #endregion test_delete_run_success


# #region test_delete_run_not_found [C:2] [TYPE Function]
# @BRIEF DELETE for nonexistent run returns 404.
def test_delete_run_not_found(mock_all_deps):
    mock_all_deps["run_service"].delete_run.return_value = False
    response = client.delete("/api/validation/runs/nonexistent")
    assert response.status_code == 404
# #endregion test_delete_run_not_found


# ── Auth / Permission Tests ────────────────────────────────────────────

# #region test_endpoints_require_authentication [C:2] [TYPE Function]
# @BRIEF Without current_user dependency, all 9 endpoints return 401.
def test_endpoints_require_authentication(mock_all_deps):
    app.dependency_overrides.pop(get_current_user, None)
    endpoints = [
        ("GET", "/api/validation/tasks"),
        ("POST", "/api/validation/tasks"),
        ("GET", "/api/validation/tasks/task-1"),
        ("PUT", "/api/validation/tasks/task-1"),
        ("DELETE", "/api/validation/tasks/task-1"),
        ("POST", "/api/validation/tasks/task-1/run"),
        ("GET", "/api/validation/runs"),
        ("GET", "/api/validation/runs/run-1"),
        ("DELETE", "/api/validation/runs/run-1"),
    ]
    for method, path in endpoints:
        response = client.request(method, path)
        assert response.status_code == 401, f"{method} {path} expected 401 got {response.status_code}"
# #endregion test_endpoints_require_authentication


# #region test_permission_denied_non_admin [C:2] [TYPE Function]
# @BRIEF A non-admin user without validation.* permissions receives 403.
def test_permission_denied_non_admin(mock_all_deps):
    """Remove Admin role so has_permission raises 403."""
    mock_user.roles = []  # No Admin, no permissions
    response = client.get("/api/validation/tasks")
    assert response.status_code == 403
# #endregion test_permission_denied_non_admin


# #region test_permission_denied_all_actions [C:2] [TYPE Function]
# @BRIEF Each action (VIEW, CREATE, EDIT, DELETE, EXECUTE) denied for non-admin.
def test_permission_denied_all_actions(mock_all_deps):
    mock_user.roles = []
    action_endpoints = [
        ("GET", "/api/validation/tasks"),
        ("POST", "/api/validation/tasks"),
        ("GET", "/api/validation/tasks/task-1"),
        ("PUT", "/api/validation/tasks/task-1"),
        ("DELETE", "/api/validation/tasks/task-1"),
        ("POST", "/api/validation/tasks/task-1/run"),
        ("GET", "/api/validation/runs"),
        ("GET", "/api/validation/runs/run-1"),
        ("DELETE", "/api/validation/runs/run-1"),
    ]
    for method, path in action_endpoints:
        response = client.request(method, path)
        assert response.status_code == 403, f"{method} {path} expected 403 got {response.status_code}"
# #endregion test_permission_denied_all_actions


# #region test_delete_task_runs_default_false [C:2] [TYPE Function]
# @BRIEF delete_runs query param defaults to False.
def test_delete_task_runs_default_false(mock_all_deps):
    mock_all_deps["task_service"].delete_task.return_value = None
    response = client.delete("/api/validation/tasks/task-1")
    assert response.status_code == 204
    mock_all_deps["task_service"].delete_task.assert_called_with("task-1", delete_runs=False)
# #endregion test_delete_task_runs_default_false

# #endregion ValidationApiTests
