"""services/saved_views — per-user named views (M8; ADR #009).

Covers: create/list ordering, rename, pin, reorder, delete, per-scope
name uniqueness (case-insensitive), page validation, the payload-absent
guard on update, per-user isolation (a view id you don't own is 404),
the 401 when no real user, and idempotent default-view seeding.
"""

import uuid

import pytest

from app.models import SavedView
from app.services import saved_views as svc
from app.services.errors import ServiceError
from tests.saved_views.conftest import actor_for

# ------------------------------------------------------------- create/list


def test_create_and_list_orders_pinned_first(session, users):
    a = actor_for(users["rian"])
    svc.create_view(session, a, page="entries", name="Zeta", search="status=all")
    svc.create_view(session, a, page="entries", name="Alpha", search="status=pending")
    svc.create_view(
        session, a, page="entries", name="Pinned one", search="date=all", pinned=True
    )

    views = svc.list_views(session, a, "entries")["views"]
    # pinned first, then sort_order asc, then name — Pinned one leads.
    assert [v["name"] for v in views] == ["Pinned one", "Zeta", "Alpha"]
    assert views[0]["pinned"] is True
    assert views[0]["search"] == "date=all"


def test_list_filters_by_page(session, users):
    a = actor_for(users["rian"])
    svc.create_view(session, a, page="entries", name="E", search="")
    svc.create_view(session, a, page="projects", name="P", search="")
    svc.create_view(session, a, page="cc", name="C", search="")

    assert [v["name"] for v in svc.list_views(session, a, "entries")["views"]] == ["E"]
    # omitted page -> every page
    assert len(svc.list_views(session, a, None)["views"]) == 3


def test_search_strips_leading_question_mark(session, users):
    a = actor_for(users["rian"])
    row = svc.create_view(
        session, a, page="entries", name="Q", search="?date=all&status=pending"
    )
    assert row["search"] == "date=all&status=pending"


# -------------------------------------------------------------- uniqueness


def test_duplicate_name_case_insensitive_rejected(session, users):
    a = actor_for(users["rian"])
    svc.create_view(session, a, page="entries", name="My View", search="")
    with pytest.raises(ServiceError) as exc:
        svc.create_view(session, a, page="entries", name="my view", search="")
    assert exc.value.code == "VIEW_NAME_EXISTS"
    assert exc.value.status == 409


def test_same_name_different_page_is_allowed(session, users):
    a = actor_for(users["rian"])
    svc.create_view(session, a, page="entries", name="Shared", search="")
    # different page -> no collision
    row = svc.create_view(session, a, page="projects", name="Shared", search="")
    assert row["page"] == "projects"


def test_same_name_different_user_is_allowed(session, users):
    rian = actor_for(users["rian"])
    adi = actor_for(users["adi"])
    svc.create_view(session, rian, page="entries", name="Backlog", search="")
    row = svc.create_view(session, adi, page="entries", name="Backlog", search="")
    assert row["name"] == "Backlog"


# -------------------------------------------------------------- validation


def test_bad_page_rejected(session, users):
    a = actor_for(users["rian"])
    with pytest.raises(ServiceError) as exc:
        svc.create_view(session, a, page="dashboard", name="x", search="")
    assert exc.value.code == "BAD_VIEW_PAGE"


def test_blank_name_rejected(session, users):
    a = actor_for(users["rian"])
    with pytest.raises(ServiceError) as exc:
        svc.create_view(session, a, page="entries", name="   ", search="")
    assert exc.value.code == "VIEW_NAME_REQUIRED"


def test_no_real_user_is_401(session, users):
    with pytest.raises(ServiceError) as exc:
        svc.create_view(
            session, actor_for(None), page="entries", name="x", search=""
        )
    assert exc.value.status == 401


# ----------------------------------------------------------------- update


def test_update_renames_and_toggles_pin(session, users):
    a = actor_for(users["rian"])
    row = svc.create_view(session, a, page="entries", name="Old", search="a=1")
    vid = uuid.UUID(row["id"])
    updated = svc.update_view(session, a, vid, name="New", pinned=True)
    assert updated["name"] == "New"
    assert updated["pinned"] is True
    # search untouched (payload-absent guard)
    assert updated["search"] == "a=1"


def test_update_only_changes_provided_fields(session, users):
    a = actor_for(users["rian"])
    row = svc.create_view(
        session, a, page="entries", name="Keep", search="date=all", pinned=True
    )
    vid = uuid.UUID(row["id"])
    updated = svc.update_view(session, a, vid, search="status=pending")
    assert updated["search"] == "status=pending"
    assert updated["name"] == "Keep"  # unchanged
    assert updated["pinned"] is True  # unchanged


def test_rename_collision_rejected(session, users):
    a = actor_for(users["rian"])
    svc.create_view(session, a, page="entries", name="One", search="")
    row2 = svc.create_view(session, a, page="entries", name="Two", search="")
    with pytest.raises(ServiceError) as exc:
        svc.update_view(session, a, uuid.UUID(row2["id"]), name="one")
    assert exc.value.code == "VIEW_NAME_EXISTS"


# ------------------------------------------------------------- isolation


def test_cannot_touch_another_users_view(session, users):
    rian = actor_for(users["rian"])
    adi = actor_for(users["adi"])
    row = svc.create_view(session, rian, page="entries", name="Rian only", search="")
    vid = uuid.UUID(row["id"])
    # adi cannot see it
    assert svc.list_views(session, adi, "entries")["views"] == []
    # ...nor update it (404, never reveal it exists)
    with pytest.raises(ServiceError) as exc:
        svc.update_view(session, adi, vid, name="hijack")
    assert exc.value.status == 404
    # ...nor delete it
    with pytest.raises(ServiceError) as exc2:
        svc.delete_view(session, adi, vid)
    assert exc2.value.status == 404


def test_delete_removes_only_the_target(session, users):
    a = actor_for(users["rian"])
    keep = svc.create_view(session, a, page="entries", name="Keep", search="")
    drop = svc.create_view(session, a, page="entries", name="Drop", search="")
    svc.delete_view(session, a, uuid.UUID(drop["id"]))
    names = [v["name"] for v in svc.list_views(session, a, "entries")["views"]]
    assert names == ["Keep"]
    assert keep["id"] != drop["id"]


# ---------------------------------------------------------------- seeding


def test_seed_default_views_idempotent(session, users):
    first = svc.seed_default_saved_views(session)
    assert first == 6  # 3 recurring entries views + 3 projects views
    rian = actor_for(users["rian"])
    entries = svc.list_views(session, rian, "entries")["views"]
    assert {v["name"] for v in entries} == {
        "Last month's PlusROI",
        "Pending backlog",
        "Blocked triage",
    }
    assert all(v["pinned"] for v in entries)
    projects = svc.list_views(session, rian, "projects")["views"]
    assert {v["name"] for v in projects} == {
        "Hourly blocks",
        "Missing info",
        "Last month admin",
    }
    # the load-bearing hourly view keeps date=all (support-block math)
    hourly = next(v for v in projects if v["name"] == "Hourly blocks")
    assert "date=all" in hourly["search"] and hourly["pinned"] is True
    # second run inserts nothing
    assert svc.seed_default_saved_views(session) == 0


def test_seed_skips_absent_users(session):
    # no users table rows -> nothing to seed, no crash
    assert svc.seed_default_saved_views(session) == 0
    assert session.query(SavedView).count() == 0
