"""
tests/test_backlog_store_postgres.py — Postgres backend unit tests (mocked psycopg2).
"""
import json
import os
import sys
import types
from unittest.mock import MagicMock, patch, call

import pytest

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "services", "router"))

from backlog_store import (
    BacklogItem, BacklogEvent, PostgresBacklogStore, _new_id, _now_iso,
)


def _make_item(**overrides) -> BacklogItem:
    base = dict(
        id=_new_id("bl"), created_at=_now_iso(), updated_at=_now_iso(),
        env="prod", service="router", category="refactor",
        title="[REF] Reduce pressure: router", description="desc",
        priority="P1", status="open", owner="cto", due_date="2026-04-01",
        source="digest",
        dedupe_key="platform_backlog:2026-W09:prod:router:refactor",
        evidence_refs={}, tags=[], meta={},
    )
    base.update(overrides)
    return BacklogItem.from_dict(base)


def _make_col(name):
    c = MagicMock()
    c.name = name
    return c


def _build_row(item: BacklogItem):
    fields = ["id", "created_at", "updated_at", "env", "service", "category",
              "title", "description", "priority", "status", "owner", "due_date",
              "source", "dedupe_key", "evidence_refs", "tags", "meta"]
    d = item.to_dict()
    row = tuple(
        json.dumps(d[f]) if f in ("evidence_refs", "tags", "meta") else d.get(f, "")
        for f in fields
    )
    desc = [_make_col(f) for f in fields]
    return row, desc


@pytest.fixture
def mock_psycopg2(monkeypatch):
    """Patch psycopg2 at the module level used by backlog_store."""
    mock_mod = MagicMock()
    monkeypatch.setattr("builtins.__import__", _make_import_patcher(mock_mod))
    return mock_mod


def _make_import_patcher(mock_pg):
    real_import = __builtins__.__import__ if hasattr(__builtins__, "__import__") else __import__

    def patched_import(name, *args, **kwargs):
        if name == "psycopg2":
            return mock_pg
        return real_import(name, *args, **kwargs)

    return patched_import


class TestPostgresCreate:
    def test_create_executes_insert(self):
        pytest.importorskip("psycopg2", reason="psycopg2 not installed")
        store = PostgresBacklogStore(dsn="postgresql://test/db")
        item = _make_item()
        mock_conn = MagicMock()
        mock_cur = MagicMock()
        mock_conn.__enter__ = MagicMock(return_value=mock_conn)
        mock_conn.__exit__ = MagicMock(return_value=False)
        mock_conn.cursor.return_value.__enter__ = MagicMock(return_value=mock_cur)
        mock_conn.cursor.return_value.__exit__ = MagicMock(return_value=False)
        with patch.object(store, "_conn", return_value=mock_conn):
            result = store.create(item)
        assert result.id == item.id
        assert mock_cur.execute.called
        sql = mock_cur.execute.call_args[0][0]
        assert "INSERT INTO backlog_items" in sql


class TestPostgresGet:
    def test_get_returns_item(self):
        store = PostgresBacklogStore(dsn="postgresql://test/db")
        item = _make_item()
        row, desc = _build_row(item)
        mock_conn = MagicMock()
        mock_cur = MagicMock()
        mock_cur.fetchone.return_value = row
        mock_cur.description = desc
        mock_conn.__enter__ = MagicMock(return_value=mock_conn)
        mock_conn.__exit__ = MagicMock(return_value=False)
        mock_conn.cursor.return_value.__enter__ = MagicMock(return_value=mock_cur)
        mock_conn.cursor.return_value.__exit__ = MagicMock(return_value=False)
        with patch.object(store, "_conn", return_value=mock_conn):
            result = store.get(item.id)
        assert result is not None
        assert result.id == item.id
        assert result.service == "router"

    def test_get_returns_none_when_missing(self):
        store = PostgresBacklogStore(dsn="postgresql://test/db")
        mock_conn = MagicMock()
        mock_cur = MagicMock()
        mock_cur.fetchone.return_value = None
        mock_conn.__enter__ = MagicMock(return_value=mock_conn)
        mock_conn.__exit__ = MagicMock(return_value=False)
        mock_conn.cursor.return_value.__enter__ = MagicMock(return_value=mock_cur)
        mock_conn.cursor.return_value.__exit__ = MagicMock(return_value=False)
        with patch.object(store, "_conn", return_value=mock_conn):
            result = store.get("no_such_id")
        assert result is None


class TestPostgresGetByDedupeKey:
    def test_get_by_dedupe_key_found(self):
        store = PostgresBacklogStore(dsn="postgresql://test/db")
        item = _make_item()
        row, desc = _build_row(item)
        mock_conn = MagicMock()
        mock_cur = MagicMock()
        mock_cur.fetchone.return_value = row
        mock_cur.description = desc
        mock_conn.__enter__ = MagicMock(return_value=mock_conn)
        mock_conn.__exit__ = MagicMock(return_value=False)
        mock_conn.cursor.return_value.__enter__ = MagicMock(return_value=mock_cur)
        mock_conn.cursor.return_value.__exit__ = MagicMock(return_value=False)
        with patch.object(store, "_conn", return_value=mock_conn):
            result = store.get_by_dedupe_key(item.dedupe_key)
        assert result is not None
        assert result.dedupe_key == item.dedupe_key


class TestPostgresUpdate:
    def test_update_executes_sql(self):
        store = PostgresBacklogStore(dsn="postgresql://test/db")
        item = _make_item()
        mock_conn = MagicMock()
        mock_cur = MagicMock()
        mock_conn.__enter__ = MagicMock(return_value=mock_conn)
        mock_conn.__exit__ = MagicMock(return_value=False)
        mock_conn.cursor.return_value.__enter__ = MagicMock(return_value=mock_cur)
        mock_conn.cursor.return_value.__exit__ = MagicMock(return_value=False)
        with patch.object(store, "_conn", return_value=mock_conn):
            store.update(item)
        sql = mock_cur.execute.call_args[0][0]
        assert "UPDATE backlog_items" in sql


class TestPostgresListItems:
    def test_list_with_env_filter(self):
        store = PostgresBacklogStore(dsn="postgresql://test/db")
        item = _make_item()
        row, desc = _build_row(item)
        mock_conn = MagicMock()
        mock_cur = MagicMock()
        mock_cur.fetchall.return_value = [row]
        mock_cur.description = desc
        mock_conn.__enter__ = MagicMock(return_value=mock_conn)
        mock_conn.__exit__ = MagicMock(return_value=False)
        mock_conn.cursor.return_value.__enter__ = MagicMock(return_value=mock_cur)
        mock_conn.cursor.return_value.__exit__ = MagicMock(return_value=False)
        with patch.object(store, "_conn", return_value=mock_conn):
            results = store.list_items({"env": "prod"})
        assert len(results) == 1
        sql = mock_cur.execute.call_args[0][0]
        assert "WHERE" in sql
        assert "env" in sql


class TestPostgresCleanup:
    def test_cleanup_runs_delete(self):
        store = PostgresBacklogStore(dsn="postgresql://test/db")
        mock_conn = MagicMock()
        mock_cur = MagicMock()
        mock_cur.rowcount = 3
        mock_conn.__enter__ = MagicMock(return_value=mock_conn)
        mock_conn.__exit__ = MagicMock(return_value=False)
        mock_conn.cursor.return_value.__enter__ = MagicMock(return_value=mock_cur)
        mock_conn.cursor.return_value.__exit__ = MagicMock(return_value=False)
        with patch.object(store, "_conn", return_value=mock_conn):
            deleted = store.cleanup(retention_days=180)
        assert deleted == 3
        sql = mock_cur.execute.call_args[0][0]
        assert "DELETE FROM backlog_items" in sql
        assert "done" in sql or "canceled" in sql
