"""
Medienarchiv: GET /api/media-assets und POST /api/exercises/{id}/media/from-asset (gemockte DB).
"""
from __future__ import annotations

import os
from unittest.mock import MagicMock, patch

import pytest
from fastapi.testclient import TestClient

os.environ.setdefault("SKIP_DB_MIGRATE", "1")

from auth import require_auth
from main import app
from tenant_context import TenantContext, get_tenant_context

# Gültige storage_key-Beispiele (64 Hex-Zeichen wie echter SHA-256)
_SK_OFF_A = f"library/official/image/{'a' * 64}.jpg"
_SK_OFF_B = f"library/official/image/{'b' * 64}.jpg"
_SK_PRIV_C = f"library/verein-c1/video/{'c' * 64}.u1.mp4"


@pytest.fixture
def client() -> TestClient:
    return TestClient(app)


@pytest.fixture(autouse=True)
def _clear_overrides() -> None:
    yield
    app.dependency_overrides.pop(require_auth, None)
    app.dependency_overrides.pop(get_tenant_context, None)


def _mock_db(mock_cur: MagicMock) -> MagicMock:
    mock_conn = MagicMock()
    mock_cm = MagicMock()
    mock_cm.__enter__.return_value = mock_conn
    mock_cm.__exit__.return_value = False
    return mock_cm


def test_list_media_assets_ok_mocked(client: TestClient) -> None:
    app.dependency_overrides[require_auth] = lambda: {"profile_id": 10, "role": "trainer"}
    app.dependency_overrides[get_tenant_context] = lambda: TenantContext(
        profile_id=10,
        global_role="trainer",
        effective_club_id=5,
        club_ids=frozenset({5}),
        memberships=[],
    )

    mock_cur = MagicMock()
    mock_cur.fetchall.side_effect = [
        [
            {
                "id": 1,
                "mime_type": "image/png",
                "byte_size": 100,
                "original_filename": "a.png",
                "visibility": "official",
                "club_id": None,
                "uploaded_by_profile_id": 2,
                "lifecycle_state": "active",
                "created_at": None,
                "sha256": "a" * 64,
                "copyright_notice": None,
                "storage_key": "media/a.png",
                "tags": ["demo"],
                "uploader_name": None,
                "uploader_email": None,
                "club_name": None,
            }
        ],
        [],
    ]
    mock_cm = _mock_db(mock_cur)

    with patch("routers.media_assets.get_db", return_value=mock_cm), patch(
        "routers.media_assets.get_cursor", return_value=mock_cur
    ), patch("routers.media_assets.club_ids_for_profile_with_roles", return_value=set()):
        r = client.get("/api/media-assets?q=test", headers={"X-Auth-Token": "t"})

    assert r.status_code == 200
    body = r.json()
    assert body["limit"] == 30
    assert len(body["items"]) == 1
    assert body["items"][0]["original_filename"] == "a.png"
    assert body["items"][0]["usage"] == {"exercises": [], "training_units": []}
    assert body["items"][0]["tags"] == ["demo"]
    assert "viewer" in body


def test_list_media_assets_media_kind_image_ok_mocked(client: TestClient) -> None:
    """media_kind=image setzt %% in SQL — Regression gegen psycopg2-%-Platzhalter."""
    app.dependency_overrides[require_auth] = lambda: {"profile_id": 10, "role": "trainer"}
    app.dependency_overrides[get_tenant_context] = lambda: TenantContext(
        profile_id=10,
        global_role="trainer",
        effective_club_id=5,
        club_ids=frozenset({5}),
        memberships=[],
    )

    mock_cur = MagicMock()
    mock_cur.fetchall.side_effect = [[], []]
    mock_cm = _mock_db(mock_cur)

    with patch("routers.media_assets.get_db", return_value=mock_cm), patch(
        "routers.media_assets.get_cursor", return_value=mock_cur
    ), patch("routers.media_assets.club_ids_for_profile_with_roles", return_value=set()):
        r = client.get("/api/media-assets?media_kind=image", headers={"X-Auth-Token": "t"})

    assert r.status_code == 200
    calls = [str(c) for c in mock_cur.execute.call_args_list]
    joined = " ".join(calls)
    assert "image/%%" in joined


def test_attach_from_asset_duplicate_returns_400(client: TestClient) -> None:
    app.dependency_overrides[require_auth] = lambda: {"profile_id": 1, "role": "trainer"}
    app.dependency_overrides[get_tenant_context] = lambda: TenantContext(
        profile_id=1,
        global_role="trainer",
        effective_club_id=None,
        club_ids=frozenset(),
        memberships=[],
    )

    mock_cur = MagicMock()
    mock_cur.fetchone.side_effect = [
        {"created_by": 1, "visibility": "private", "club_id": None},  # _assert_can_edit_exercise
        {"c": 0},                                                       # _count_exercise_media
        {
            "id": 5,
            "mime_type": "image/jpeg",
            "byte_size": 10,
            "original_filename": "x.jpg",
            "visibility": "official",
            "club_id": None,
            "uploaded_by_profile_id": 1,
            "lifecycle_state": "active",
                "storage_key": _SK_OFF_A,
        },                                                              # asset lookup
        {"visibility": "private"},                                      # P-06: exercise visibility
        {"id": 1},                                                      # duplicate check -> 400
    ]
    mock_cm = _mock_db(mock_cur)

    with patch("routers.exercises.get_db", return_value=mock_cm), patch(
        "routers.exercises.get_cursor", return_value=mock_cur
    ), patch("routers.exercises.assert_rights_for_exercise_link"):
        r = client.post(
            "/api/exercises/3/media/from-asset",
            headers={"X-Auth-Token": "t", "Content-Type": "application/json"},
            json={
                "media_asset_id": 5,
                "title": "",
                "description": "",
                "context": "ablauf",
                "is_primary": False,
            },
        )

    assert r.status_code == 400
    assert "bereits" in (r.json().get("detail") or "").lower()


def test_attach_from_asset_ok_mocked(client: TestClient) -> None:
    app.dependency_overrides[require_auth] = lambda: {"profile_id": 1, "role": "trainer"}
    app.dependency_overrides[get_tenant_context] = lambda: TenantContext(
        profile_id=1,
        global_role="trainer",
        effective_club_id=None,
        club_ids=frozenset(),
        memberships=[],
    )

    inserted = {
        "id": 99,
        "exercise_id": 3,
        "media_type": "image",
        "file_path": f"/media/{_SK_OFF_B}",
        "file_size": 10,
        "mime_type": "image/jpeg",
        "original_filename": "h.jpg",
        "embed_url": None,
        "embed_platform": None,
        "title": "h.jpg",
        "description": None,
        "sort_order": 1,
        "is_primary": False,
        "context": "ablauf",
        "created_at": None,
        "media_asset_id": 5,
    }
    mock_cur = MagicMock()
    mock_cur.fetchone.side_effect = [
        {"created_by": 1, "visibility": "private", "club_id": None},  # _assert_can_edit_exercise
        {"c": 0},                                                       # _count_exercise_media
        {
            "id": 5,
            "mime_type": "image/jpeg",
            "byte_size": 10,
            "original_filename": "h.jpg",
            "visibility": "official",
            "club_id": None,
            "uploaded_by_profile_id": 1,
            "lifecycle_state": "active",
            "storage_key": _SK_OFF_B,
        },                                                              # asset lookup
        {"visibility": "private"},                                      # P-06: exercise visibility
        None,                                                           # duplicate check -> None (no duplicate)
        inserted,                                                       # INSERT RETURNING
    ]
    mock_cm = _mock_db(mock_cur)

    with patch("routers.exercises.get_db", return_value=mock_cm), patch(
        "routers.exercises.get_cursor", return_value=mock_cur
    ), patch("routers.exercises.assert_rights_for_exercise_link"):
        r = client.post(
            "/api/exercises/3/media/from-asset",
            headers={"X-Auth-Token": "t", "Content-Type": "application/json"},
            json={
                "media_asset_id": 5,
                "context": "detail",
                "is_primary": False,
            },
        )

    assert r.status_code == 201
    body = r.json()
    assert body["id"] == 99
    assert body["media_asset_id"] == 5
    assert body["asset_lifecycle_state"] == "active"


def test_delete_exercise_media_returns_orphan_when_last_ref(client: TestClient) -> None:
    app.dependency_overrides[require_auth] = lambda: {"profile_id": 1, "role": "trainer"}
    app.dependency_overrides[get_tenant_context] = lambda: TenantContext(
        profile_id=1,
        global_role="trainer",
        effective_club_id=None,
        club_ids=frozenset(),
        memberships=[],
    )

    mock_cur = MagicMock()
    mock_cur.fetchone.side_effect = [
        {"created_by": 1, "visibility": "private", "club_id": None},
        {"media_asset_id": 88},
        {"c": 1},
    ]
    mock_cm = _mock_db(mock_cur)

    with patch("routers.exercises.get_db", return_value=mock_cm), patch(
        "routers.exercises.get_cursor", return_value=mock_cur
    ):
        r = client.delete("/api/exercises/10/media/20", headers={"X-Auth-Token": "t"})

    assert r.status_code == 200
    body = r.json()
    assert body["ok"] is True
    assert body["orphan_media_asset_id"] == 88


def test_delete_exercise_media_no_orphan_when_shared(client: TestClient) -> None:
    app.dependency_overrides[require_auth] = lambda: {"profile_id": 1, "role": "trainer"}
    app.dependency_overrides[get_tenant_context] = lambda: TenantContext(
        profile_id=1,
        global_role="trainer",
        effective_club_id=None,
        club_ids=frozenset(),
        memberships=[],
    )

    mock_cur = MagicMock()
    mock_cur.fetchone.side_effect = [
        {"created_by": 1, "visibility": "private", "club_id": None},
        {"media_asset_id": 88},
        {"c": 2},
    ]
    mock_cm = _mock_db(mock_cur)

    with patch("routers.exercises.get_db", return_value=mock_cm), patch(
        "routers.exercises.get_cursor", return_value=mock_cur
    ):
        r = client.delete("/api/exercises/10/media/20", headers={"X-Auth-Token": "t"})

    assert r.status_code == 200
    assert r.json()["orphan_media_asset_id"] is None


def test_delete_exercise_media_embed_row_no_orphan(client: TestClient) -> None:
    app.dependency_overrides[require_auth] = lambda: {"profile_id": 1, "role": "trainer"}
    app.dependency_overrides[get_tenant_context] = lambda: TenantContext(
        profile_id=1,
        global_role="trainer",
        effective_club_id=None,
        club_ids=frozenset(),
        memberships=[],
    )

    mock_cur = MagicMock()
    mock_cur.fetchone.side_effect = [
        {"created_by": 1, "visibility": "private", "club_id": None},
        {"media_asset_id": None},
    ]
    mock_cm = _mock_db(mock_cur)

    with patch("routers.exercises.get_db", return_value=mock_cm), patch(
        "routers.exercises.get_cursor", return_value=mock_cur
    ):
        r = client.delete("/api/exercises/10/media/21", headers={"X-Auth-Token": "t"})

    assert r.status_code == 200
    assert r.json() == {"ok": True, "orphan_media_asset_id": None}


def test_media_asset_lifecycle_reactivate_mocked(client: TestClient) -> None:
    app.dependency_overrides[require_auth] = lambda: {"profile_id": 1, "role": "trainer"}
    app.dependency_overrides[get_tenant_context] = lambda: TenantContext(
        profile_id=1,
        global_role="trainer",
        effective_club_id=None,
        club_ids=frozenset(),
        memberships=[],
    )

    mock_cur = MagicMock()
    mock_cur.fetchone.side_effect = [
        {
            "id": 5,
            "visibility": "private",
            "club_id": 1,
            "uploaded_by_profile_id": 1,
            "lifecycle_state": "trash_soft",
            "storage_key": _SK_PRIV_C,
            "storage_backend": "local",
            "trash_soft_at": None,
            "trash_hidden_at": None,
            "purge_after_at": None,
        },
        {"id": 5, "lifecycle_state": "active"},
    ]
    mock_cm = _mock_db(mock_cur)

    with patch("routers.media_assets.get_db", return_value=mock_cm), patch(
        "routers.media_assets.get_cursor", return_value=mock_cur
    ):
        r = client.post(
            "/api/media-assets/5/lifecycle",
            headers={"X-Auth-Token": "t", "Content-Type": "application/json"},
            json={"action": "reactivate"},
        )

    assert r.status_code == 200
    assert r.json()["lifecycle_state"] == "active"


def test_media_asset_lifecycle_purge_forbidden_for_non_superadmin(client: TestClient) -> None:
    app.dependency_overrides[require_auth] = lambda: {"profile_id": 1, "role": "admin"}
    app.dependency_overrides[get_tenant_context] = lambda: TenantContext(
        profile_id=1,
        global_role="admin",
        effective_club_id=None,
        club_ids=frozenset(),
        memberships=[],
    )

    mock_cur = MagicMock()
    mock_cm = _mock_db(mock_cur)

    fake_row = {
        "id": 99,
        "visibility": "official",
        "club_id": None,
        "uploaded_by_profile_id": 2,
        "lifecycle_state": "trash_hidden",
        "storage_key": "x",
        "storage_backend": "local",
        "trash_soft_at": None,
        "trash_hidden_at": None,
        "purge_after_at": None,
    }

    with patch("routers.media_assets.get_db", return_value=mock_cm), patch(
        "routers.media_assets.get_cursor", return_value=mock_cur
    ), patch("routers.media_assets.fetch_media_asset_row", return_value=fake_row):
        r = client.post(
            "/api/media-assets/99/lifecycle",
            headers={"X-Auth-Token": "t", "Content-Type": "application/json"},
            json={"action": "purge"},
        )

    assert r.status_code == 403
    assert "Superadmin" in (r.json().get("detail") or "")


def test_list_media_assets_lifecycle_trash_soft_mocked(client: TestClient) -> None:
    app.dependency_overrides[require_auth] = lambda: {"profile_id": 10, "role": "trainer"}
    app.dependency_overrides[get_tenant_context] = lambda: TenantContext(
        profile_id=10,
        global_role="trainer",
        effective_club_id=5,
        club_ids=frozenset({5}),
        memberships=[],
    )
    mock_cur = MagicMock()
    mock_cur.fetchall.return_value = []
    mock_cm = _mock_db(mock_cur)
    with patch("routers.media_assets.get_db", return_value=mock_cm), patch(
        "routers.media_assets.get_cursor", return_value=mock_cur
    ), patch("routers.media_assets.club_ids_for_profile_with_roles", return_value=set()):
        r = client.get("/api/media-assets?lifecycle=trash_soft", headers={"X-Auth-Token": "t"})
    assert r.status_code == 200
    assert r.json()["lifecycle"] == "trash_soft"
    list_sql_calls = [c[0][0] for c in mock_cur.execute.call_args_list if c[0] and "FROM media_assets ma" in str(c[0][0])]
    assert list_sql_calls and "trash_soft" in list_sql_calls[0]


def test_list_media_assets_invalid_media_kind_400(client: TestClient) -> None:
    app.dependency_overrides[require_auth] = lambda: {"profile_id": 10, "role": "trainer"}
    app.dependency_overrides[get_tenant_context] = lambda: TenantContext(
        profile_id=10,
        global_role="trainer",
        effective_club_id=None,
        club_ids=frozenset(),
        memberships=[],
    )
    r = client.get("/api/media-assets?media_kind=movies", headers={"X-Auth-Token": "t"})
    assert r.status_code == 400


def test_list_media_assets_club_filter_forbidden_non_superadmin(client: TestClient) -> None:
    app.dependency_overrides[require_auth] = lambda: {"profile_id": 10, "role": "admin"}
    app.dependency_overrides[get_tenant_context] = lambda: TenantContext(
        profile_id=10,
        global_role="admin",
        effective_club_id=None,
        club_ids=frozenset(),
        memberships=[],
    )
    r = client.get("/api/media-assets?club_id=3", headers={"X-Auth-Token": "t"})
    assert r.status_code == 403


def test_list_media_assets_invalid_lifecycle_400(client: TestClient) -> None:
    app.dependency_overrides[require_auth] = lambda: {"profile_id": 10, "role": "trainer"}
    app.dependency_overrides[get_tenant_context] = lambda: TenantContext(
        profile_id=10,
        global_role="trainer",
        effective_club_id=None,
        club_ids=frozenset(),
        memberships=[],
    )
    r = client.get("/api/media-assets?lifecycle=invalid", headers={"X-Auth-Token": "t"})
    assert r.status_code == 400


def test_patch_media_asset_copyright_mocked(client: TestClient) -> None:
    app.dependency_overrides[require_auth] = lambda: {"profile_id": 10, "role": "trainer"}
    app.dependency_overrides[get_tenant_context] = lambda: TenantContext(
        profile_id=10,
        global_role="trainer",
        effective_club_id=None,
        club_ids=frozenset(),
        memberships=[],
    )
    mock_cur = MagicMock()
    mock_cur.fetchone.side_effect = [
        {
            "id": 9,
            "visibility": "private",
            "club_id": None,
            "uploaded_by_profile_id": 10,
            "lifecycle_state": "active",
            "copyright_notice": "",
            "original_filename": "x.png",
        },
        {
            "id": 9,
            "mime_type": "image/png",
            "byte_size": 10,
            "original_filename": "x.png",
            "visibility": "private",
            "club_id": None,
            "uploaded_by_profile_id": 10,
            "lifecycle_state": "active",
            "created_at": None,
            "sha256": "b" * 64,
            "copyright_notice": "© HoldCo",
            "tags": [],
        },
    ]
    mock_cm = _mock_db(mock_cur)
    with patch("routers.media_assets.get_db", return_value=mock_cm), patch(
        "routers.media_assets.get_cursor", return_value=mock_cur
    ), patch("media_lifecycle.assert_can_manage_media_asset_lifecycle", lambda *a, **k: None):
        r = client.patch(
            "/api/media-assets/9",
            headers={"X-Auth-Token": "t", "Content-Type": "application/json"},
            json={"copyright_notice": "© HoldCo"},
        )
    assert r.status_code == 200
    assert r.json()["copyright_notice"] == "© HoldCo"

