"""Tests for FastAPI server routes."""

import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import StaticPool

from evalkit.server.app import create_app
from evalkit.server.db.base import Base
from evalkit.server.db.session import get_db


def override_get_db(engine):
    Session = sessionmaker(bind=engine)

    def _get_db():
        db = Session()
        try:
            yield db
        finally:
            db.close()

    return _get_db


@pytest.fixture
def client():
    # StaticPool ensures all connections share the same in-memory database so
    # that tables created by create_all() are visible to query sessions.
    engine = create_engine(
        "sqlite:///:memory:",
        connect_args={"check_same_thread": False},
        poolclass=StaticPool,
    )
    Base.metadata.create_all(engine)
    fastapi_app = create_app()
    fastapi_app.dependency_overrides[get_db] = override_get_db(engine)
    # Do NOT use TestClient as a context manager — that would trigger the
    # lifespan which calls init_db() on the wrong (file-based) engine.
    # Routes don't need the lifespan; tables are already on our test engine.
    c = TestClient(fastapi_app, raise_server_exceptions=False)
    yield c
    Base.metadata.drop_all(engine)


def test_dashboard_returns_200(client):
    """GET / returns 200."""
    response = client.get("/")
    assert response.status_code == 200


def test_create_project_redirects(client):
    """POST /projects creates a project and redirects to /projects/{id}."""
    response = client.post(
        "/projects",
        data={"name": "test-project", "description": "A test"},
        follow_redirects=False,
    )
    # 303 See Other redirect
    assert response.status_code == 303
    assert response.headers["location"].startswith("/projects/")


def test_create_project_and_view(client):
    """POST /projects then GET /projects/{id} returns 200."""
    post_response = client.post(
        "/projects",
        data={"name": "my-project", "description": "desc"},
        follow_redirects=True,
    )
    assert post_response.status_code == 200


def test_get_project_not_found(client):
    """GET /projects/999 returns 404."""
    response = client.get("/projects/999")
    assert response.status_code == 404


def test_get_project_existing(client):
    """GET /projects/{id} returns 200 for an existing project."""
    # Create the project first
    post_response = client.post(
        "/projects",
        data={"name": "existing-project"},
        follow_redirects=False,
    )
    assert post_response.status_code == 303
    location = post_response.headers["location"]
    response = client.get(location)
    assert response.status_code == 200


def test_create_duplicate_project_returns_409(client):
    """POST /projects with duplicate name returns 409."""
    client.post("/projects", data={"name": "duplicate"})
    response = client.post("/projects", data={"name": "duplicate"})
    assert response.status_code == 409


def test_api_evaluate_project_not_found(client):
    """POST /api/v1/evaluate with nonexistent project_id returns 404."""
    response = client.post(
        "/api/v1/evaluate",
        json={
            "project_id": 9999,
            "suite": "rag_qa",
            "test_cases": [
                {
                    "question": "What is AI?",
                    "context": ["AI is artificial intelligence"],
                    "answer": "AI stands for Artificial Intelligence",
                }
            ],
        },
    )
    assert response.status_code == 404


def test_api_evaluate_returns_202(client):
    """POST /api/v1/evaluate with valid project returns 202 with evaluation_id."""
    # Create a project first
    post_response = client.post(
        "/projects",
        data={"name": "api-project"},
        follow_redirects=False,
    )
    assert post_response.status_code == 303
    location = post_response.headers["location"]
    project_id = int(location.rsplit("/", 1)[-1])

    response = client.post(
        "/api/v1/evaluate",
        json={
            "project_id": project_id,
            "suite": "rag_qa",
            "test_cases": [
                {
                    "question": "What is AI?",
                    "context": ["AI is artificial intelligence"],
                    "answer": "AI stands for Artificial Intelligence",
                }
            ],
        },
    )
    assert response.status_code == 202
    data = response.json()
    assert "evaluation_id" in data
    assert data["status"] == "pending"


def test_api_get_evaluation_status(client):
    """GET /api/v1/evaluations/{id} returns evaluation status."""
    # Create a project
    post_response = client.post(
        "/projects",
        data={"name": "status-project"},
        follow_redirects=False,
    )
    location = post_response.headers["location"]
    project_id = int(location.rsplit("/", 1)[-1])

    # Start an evaluation
    eval_response = client.post(
        "/api/v1/evaluate",
        json={
            "project_id": project_id,
            "suite": "rag_qa",
            "test_cases": [
                {
                    "question": "Test?",
                    "context": ["context"],
                    "answer": "answer",
                }
            ],
        },
    )
    assert eval_response.status_code == 202
    evaluation_id = eval_response.json()["evaluation_id"]

    # Get status
    status_response = client.get(f"/api/v1/evaluations/{evaluation_id}")
    assert status_response.status_code == 200
    status_data = status_response.json()
    assert status_data["id"] == evaluation_id
    assert "status" in status_data
    assert "suite_name" in status_data


def test_api_get_evaluation_not_found(client):
    """GET /api/v1/evaluations/9999 returns 404."""
    response = client.get("/api/v1/evaluations/9999")
    assert response.status_code == 404


def test_evaluation_detail_not_found(client):
    """GET /evaluations/9999 returns 404."""
    response = client.get("/evaluations/9999")
    assert response.status_code == 404


def test_api_config_returns_defaults(client):
    """GET /api/v1/config returns server configuration."""
    response = client.get("/api/v1/config")
    assert response.status_code == 200
    data = response.json()
    assert "judge_provider" in data
    assert "suite" in data
    assert "available_suites" in data
    assert "available_providers" in data
    assert "rag_qa" in data["available_suites"]
    assert "openai" in data["available_providers"]


def test_api_create_project(client):
    """POST /api/v1/projects creates a project and returns JSON."""
    response = client.post(
        "/api/v1/projects",
        json={"name": "rest-project", "description": "Created via REST"},
    )
    assert response.status_code == 201
    data = response.json()
    assert data["name"] == "rest-project"
    assert data["description"] == "Created via REST"
    assert "id" in data


def test_api_create_project_duplicate_returns_409(client):
    """POST /api/v1/projects with duplicate name returns 409."""
    client.post("/api/v1/projects", json={"name": "dup-project"})
    response = client.post("/api/v1/projects", json={"name": "dup-project"})
    assert response.status_code == 409


def test_api_list_projects_empty(client):
    """GET /api/v1/projects returns empty list when no projects exist."""
    response = client.get("/api/v1/projects")
    assert response.status_code == 200
    assert response.json() == []


def test_api_list_projects(client):
    """GET /api/v1/projects returns all projects."""
    client.post("/api/v1/projects", json={"name": "project-a"})
    client.post("/api/v1/projects", json={"name": "project-b"})
    response = client.get("/api/v1/projects")
    assert response.status_code == 200
    data = response.json()
    assert len(data) == 2
    names = {p["name"] for p in data}
    assert names == {"project-a", "project-b"}


def test_api_create_project_then_evaluate(client):
    """Full REST flow: create project via API, then submit evaluation."""
    proj_response = client.post(
        "/api/v1/projects",
        json={"name": "full-flow-project"},
    )
    assert proj_response.status_code == 201
    project_id = proj_response.json()["id"]

    eval_response = client.post(
        "/api/v1/evaluate",
        json={
            "project_id": project_id,
            "suite": "rag_qa",
            "test_cases": [
                {
                    "question": "What is X?",
                    "context": ["X is Y"],
                    "answer": "X is Y",
                }
            ],
        },
    )
    assert eval_response.status_code == 202
    assert eval_response.json()["status"] == "pending"


def test_api_upload_results(client):
    """POST /api/v1/results uploads local results and creates a complete evaluation."""
    proj = client.post("/api/v1/projects", json={"name": "upload-proj"})
    project_id = proj.json()["id"]

    response = client.post(
        "/api/v1/results",
        json={
            "project_id": project_id,
            "suite_name": "rag_qa",
            "results": [
                {
                    "test_case": {"question": "Q", "context": ["C"], "answer": "A"},
                    "scores": [
                        {"metric_name": "faithfulness", "score": 0.9, "passed": True, "reason": "Good"},
                    ],
                    "overall_score": 0.9,
                },
            ],
            "average_score": 0.9,
        },
    )
    assert response.status_code == 201
    data = response.json()
    assert data["status"] == "complete"
    assert "evaluation_id" in data


def test_api_upload_results_persists_scores(client):
    """Uploaded results are retrievable via the evaluation detail endpoint."""
    proj = client.post("/api/v1/projects", json={"name": "persist-proj"})
    project_id = proj.json()["id"]

    upload = client.post(
        "/api/v1/results",
        json={
            "project_id": project_id,
            "suite_name": "document_search",
            "results": [
                {
                    "test_case": {"question": "Q1", "context": ["C"], "answer": "A1"},
                    "scores": [
                        {"metric_name": "faithfulness", "score": 0.8, "passed": True, "reason": "OK"},
                        {"metric_name": "relevancy", "score": 0.6, "passed": True, "reason": "Decent"},
                    ],
                    "overall_score": 0.7,
                },
                {
                    "test_case": {"question": "Q2", "context": ["C"], "answer": "A2"},
                    "scores": [
                        {"metric_name": "faithfulness", "score": 0.5, "passed": False, "reason": "Weak"},
                    ],
                    "overall_score": 0.5,
                },
            ],
        },
    )
    evaluation_id = upload.json()["evaluation_id"]

    status = client.get(f"/api/v1/evaluations/{evaluation_id}")
    assert status.status_code == 200
    data = status.json()
    assert data["status"] == "complete"
    assert data["suite_name"] == "document_search"
    assert data["average_score"] == pytest.approx(0.6)


def test_api_upload_results_computes_average_when_omitted(client):
    """When average_score is not provided, it's computed from results."""
    proj = client.post("/api/v1/projects", json={"name": "avg-proj"})
    project_id = proj.json()["id"]

    upload = client.post(
        "/api/v1/results",
        json={
            "project_id": project_id,
            "suite_name": "rag_qa",
            "results": [
                {
                    "test_case": {"question": "Q1", "context": ["C"], "answer": "A"},
                    "scores": [],
                    "overall_score": 0.8,
                },
                {
                    "test_case": {"question": "Q2", "context": ["C"], "answer": "A"},
                    "scores": [],
                    "overall_score": 0.6,
                },
            ],
        },
    )
    evaluation_id = upload.json()["evaluation_id"]
    status = client.get(f"/api/v1/evaluations/{evaluation_id}")
    assert status.json()["average_score"] == pytest.approx(0.7)


def test_api_upload_results_project_not_found(client):
    """POST /api/v1/results with nonexistent project returns 404."""
    response = client.post(
        "/api/v1/results",
        json={
            "project_id": 9999,
            "suite_name": "rag_qa",
            "results": [],
        },
    )
    assert response.status_code == 404


def test_api_upload_results_downloadable_report(client):
    """Uploaded results are accessible via the report download endpoint."""
    proj = client.post("/api/v1/projects", json={"name": "report-proj"})
    project_id = proj.json()["id"]

    upload = client.post(
        "/api/v1/results",
        json={
            "project_id": project_id,
            "suite_name": "rag_qa",
            "results": [
                {
                    "test_case": {"question": "Q", "context": ["C"], "answer": "A"},
                    "scores": [
                        {"metric_name": "faithfulness", "score": 0.95, "passed": True, "reason": "Great"},
                    ],
                    "overall_score": 0.95,
                },
            ],
            "average_score": 0.95,
        },
    )
    evaluation_id = upload.json()["evaluation_id"]

    report = client.get(f"/evaluations/{evaluation_id}/report")
    assert report.status_code == 200
    data = report.json()
    assert data["suite_name"] == "rag_qa"
    assert data["average_score"] == pytest.approx(0.95)
    assert len(data["results"]) == 1
    assert data["results"][0]["metric_scores"][0]["metric_name"] == "faithfulness"
    assert "summary" in data
    assert data["summary"]["total_cases"] == 1
    assert "failures" in data


def test_evaluate_with_tags(client):
    client.post("/api/v1/projects", json={"name": "tag-eval-proj"})
    resp = client.post(
        "/api/v1/evaluate",
        json={
            "project_id": 1,
            "test_cases": [{"question": "Q?", "context": ["C"], "answer": "A"}],
            "tags": {"version": "v2", "branch": "main"},
        },
    )
    assert resp.status_code == 202
    eval_resp = client.get("/api/v1/evaluations/1")
    assert eval_resp.status_code == 200
    tags = eval_resp.json()["tags"]
    # User-provided tags are present
    assert tags["version"] == "v2"
    assert tags["branch"] == "main"
    # Server-side auto-tags are also present
    assert tags["suite"] == "rag_qa"
    assert "judge_provider" in tags


def test_upload_results_with_tags(client):
    client.post("/api/v1/projects", json={"name": "tag-upload-proj"})
    resp = client.post(
        "/api/v1/results",
        json={
            "project_id": 1,
            "suite_name": "rag_qa",
            "results": [
                {
                    "test_case": {"question": "Q?", "context": ["C"], "answer": "A"},
                    "scores": [{"metric_name": "faithfulness", "score": 0.9, "passed": True, "reason": "ok"}],
                    "overall_score": 0.9,
                }
            ],
            "tags": {"commit": "abc123"},
        },
    )
    assert resp.status_code == 201
    eval_resp = client.get(f"/api/v1/evaluations/{resp.json()['evaluation_id']}")
    tags = eval_resp.json()["tags"]
    assert tags["commit"] == "abc123"
    assert tags["suite"] == "rag_qa"  # server auto-tag


def test_evaluate_without_tags_has_auto_tags_only(client):
    client.post("/api/v1/projects", json={"name": "no-tag-proj"})
    client.post(
        "/api/v1/results",
        json={
            "project_id": 1,
            "suite_name": "rag_qa",
            "results": [],
        },
    )
    eval_resp = client.get("/api/v1/evaluations/1")
    tags = eval_resp.json()["tags"]
    assert tags["suite"] == "rag_qa"  # auto-tag always present


def test_update_tags_merge_semantics(client):
    client.post("/api/v1/projects", json={"name": "merge-tag-proj"})
    client.post(
        "/api/v1/results",
        json={
            "project_id": 1,
            "suite_name": "rag_qa",
            "results": [],
            "tags": {"model": "gpt-4o", "branch": "main"},
        },
    )
    # Update: change model, add new tag, leave branch untouched
    resp = client.put(
        "/api/v1/evaluations/1/tags",
        json={
            "tags": {"model": "gpt-4o-mini", "env": "staging"},
        },
    )
    assert resp.status_code == 200
    assert resp.json()["tags"]["model"] == "gpt-4o-mini"
    assert resp.json()["tags"]["branch"] == "main"
    assert resp.json()["tags"]["env"] == "staging"
    assert resp.json()["tags"]["suite"] == "rag_qa"


def test_update_tags_delete_with_null(client):
    client.post("/api/v1/projects", json={"name": "del-tag-proj"})
    client.post(
        "/api/v1/results",
        json={
            "project_id": 1,
            "suite_name": "rag_qa",
            "results": [],
            "tags": {"model": "gpt-4o", "branch": "main"},
        },
    )
    resp = client.put(
        "/api/v1/evaluations/1/tags",
        json={
            "tags": {"branch": None},
        },
    )
    assert resp.status_code == 200
    assert resp.json()["tags"] == {"model": "gpt-4o", "suite": "rag_qa"}


def test_update_tags_404(client):
    resp = client.put("/api/v1/evaluations/999/tags", json={"tags": {"a": "b"}})
    assert resp.status_code == 404


def test_evaluate_auto_tags_judge_and_suite(client):
    """Server-side auto-tags: judge_provider, judge_model, suite are set from request."""
    client.post("/api/v1/projects", json={"name": "auto-tag-proj"})
    client.post(
        "/api/v1/results",
        json={
            "project_id": 1,
            "suite_name": "rag_qa",
            "results": [],
        },
    )
    eval_resp = client.get("/api/v1/evaluations/1")
    tags = eval_resp.json()["tags"]
    assert tags["suite"] == "rag_qa"


def _create_complete_evaluation(
    client, project_id, question="What is Python?", answer="A language", score=0.9, tags=None
):
    """Helper to create a complete evaluation with one result."""
    body = {
        "project_id": project_id,
        "suite_name": "rag_qa",
        "results": [
            {
                "test_case": {"question": question, "context": ["ctx"], "answer": answer},
                "scores": [{"metric_name": "faithfulness", "score": score, "passed": score >= 0.5, "reason": "ok"}],
                "overall_score": score,
            }
        ],
    }
    if tags:
        body["tags"] = tags
    resp = client.post("/api/v1/results", json=body)
    return resp.json()["evaluation_id"]


def test_compare_evaluations_json(client):
    client.post("/api/v1/projects", json={"name": "cmp-proj"})
    id1 = _create_complete_evaluation(client, 1, score=0.7, tags={"model": "gpt-4o"})
    id2 = _create_complete_evaluation(client, 1, score=0.9, tags={"model": "gpt-4o-mini"})

    resp = client.get(f"/api/v1/projects/1/compare?ids={id1},{id2}&baseline={id1}")
    assert resp.status_code == 200
    data = resp.json()
    assert data["baseline"]["id"] == id1
    assert len(data["comparisons"]) == 1
    assert len(data["questions"]) == 1
    assert data["questions"][0]["comparison_results"][0]["score_delta"] == pytest.approx(0.2)


def test_compare_fewer_than_2_ids(client):
    client.post("/api/v1/projects", json={"name": "cmp-few-proj"})
    id1 = _create_complete_evaluation(client, 1)
    resp = client.get(f"/api/v1/projects/1/compare?ids={id1}")
    assert resp.status_code == 400


def test_compare_wrong_project(client):
    client.post("/api/v1/projects", json={"name": "cmp-proj-a"})
    client.post("/api/v1/projects", json={"name": "cmp-proj-b"})
    id1 = _create_complete_evaluation(client, 1)
    id2 = _create_complete_evaluation(client, 2)
    resp = client.get(f"/api/v1/projects/1/compare?ids={id1},{id2}")
    assert resp.status_code == 422


def test_compare_non_complete_evaluation(client):
    client.post("/api/v1/projects", json={"name": "cmp-pending-proj"})
    id1 = _create_complete_evaluation(client, 1)
    # Create a pending evaluation via evaluate endpoint
    resp = client.post(
        "/api/v1/evaluate",
        json={
            "project_id": 1,
            "test_cases": [{"question": "Q?", "context": ["C"], "answer": "A"}],
        },
    )
    id2 = resp.json()["evaluation_id"]
    resp = client.get(f"/api/v1/projects/1/compare?ids={id1},{id2}")
    assert resp.status_code == 422


def test_compare_project_not_found(client):
    resp = client.get("/api/v1/projects/999/compare?ids=1,2")
    assert resp.status_code == 404


def test_compare_baseline_not_in_ids(client):
    client.post("/api/v1/projects", json={"name": "cmp-bad-baseline-proj"})
    id1 = _create_complete_evaluation(client, 1)
    id2 = _create_complete_evaluation(client, 1)
    resp = client.get(f"/api/v1/projects/1/compare?ids={id1},{id2}&baseline=999")
    assert resp.status_code == 400


def test_compare_baseline_defaults_to_earliest(client):
    client.post("/api/v1/projects", json={"name": "cmp-default-proj"})
    id1 = _create_complete_evaluation(client, 1, score=0.7)
    id2 = _create_complete_evaluation(client, 1, score=0.9)
    resp = client.get(f"/api/v1/projects/1/compare?ids={id2},{id1}")
    assert resp.status_code == 200
    data = resp.json()
    assert data["baseline"]["id"] == id1  # earliest


def test_compare_html_renders(client):
    client.post("/api/v1/projects", json={"name": "cmp-html-proj"})
    id1 = _create_complete_evaluation(client, 1, score=0.7)
    id2 = _create_complete_evaluation(client, 1, score=0.9)

    resp = client.get(f"/projects/1/compare?ids={id1},{id2}&baseline={id1}")
    assert resp.status_code == 200
    assert "Compare Evaluations" in resp.text
    assert "BASELINE" in resp.text
