"""
Tests для Humanized Stepan v2.8 — Multi-user Farm Model.

Покриває:
1. FarmProfile keyed by chat_id (новий ключ farm_profile:agromatrix:chat:{chat_id})
2. Lazy migration: legacy key → chat-key (write-through)
3. Два users одного chat_id ділять один FarmProfile
4. Conflict detection: наступний user з відмінним legacy → лише лог, не перезаписує
5. Acceptance сценарій: userA оновлює farm, userB бачить ті ж дані
6. backward-compat: load_farm_profile без user_id → повертає default (не крашить)
"""

import sys
from pathlib import Path
from unittest.mock import patch, MagicMock
from copy import deepcopy

root = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(root))
sys.path.insert(0, str(root / 'packages' / 'agromatrix-tools'))

from crews.agromatrix_crew.memory_manager import (
    _default_farm_profile,
    _chat_fact_key,
    _legacy_farm_fact_key,
    _farm_profiles_differ,
    migrate_farm_profile_legacy_to_chat,
    load_farm_profile,
    save_farm_profile,
    _cache_set,
    _cache_get,
)
from crews.agromatrix_crew.telemetry import TELEMETRY_TAG


# ─── Helpers ──────────────────────────────────────────────────────────────────

def _make_farm(chat_id: str, crops: list | None = None) -> dict:
    p = _default_farm_profile(chat_id)
    if crops:
        p["crops"] = crops
    return p


# ─── Unit: key helpers ────────────────────────────────────────────────────────

def test_chat_fact_key_format():
    key = _chat_fact_key("123456")
    assert key == "farm_profile:agromatrix:chat:123456"


def test_legacy_fact_key_format():
    key = _legacy_farm_fact_key("user_42")
    assert key == "farm_profile:agromatrix:user_42"


def test_chat_key_differs_from_legacy():
    assert _chat_fact_key("999") != _legacy_farm_fact_key("999")


# ─── Unit: _farm_profiles_differ ─────────────────────────────────────────────

def test_farm_profiles_identical_not_differ():
    a = _make_farm("chat1", crops=["пшениця"])
    b = _make_farm("chat1", crops=["пшениця"])
    assert _farm_profiles_differ(a, b) is False


def test_farm_profiles_different_crops():
    a = _make_farm("chat1", crops=["пшениця"])
    b = _make_farm("chat1", crops=["кукурудза"])
    assert _farm_profiles_differ(a, b) is True


def test_farm_profiles_different_region():
    a = _make_farm("chat1")
    b = _make_farm("chat1")
    a["region"] = "Полтавська"
    b["region"] = "Харківська"
    assert _farm_profiles_differ(a, b) is True


def test_farm_profiles_metadata_ignored():
    """Відмінності у updated_at та _version не впливають на результат."""
    a = _make_farm("chat1", crops=["ріпак"])
    b = deepcopy(a)
    b["updated_at"] = "2026-01-01T00:00:00"
    b["_version"] = 99
    assert _farm_profiles_differ(a, b) is False


# ─── Unit: default farm profile ───────────────────────────────────────────────

def test_default_farm_version_5():
    p = _default_farm_profile("chat_x")
    assert p["_version"] == 5


def test_default_farm_has_new_fields():
    p = _default_farm_profile("chat_x")
    for field in ("farm_name", "field_ids", "crop_ids", "active_integrations",
                  "iot_sensors", "alert_thresholds", "seasonal_context"):
        assert field in p, f"Missing field: {field}"


def test_default_farm_chat_id():
    p = _default_farm_profile("chat_99")
    assert p["chat_id"] == "chat_99"


# ─── Unit: load_farm_profile — no memory-service (cache only) ─────────────────

def test_load_farm_profile_from_cache():
    """Якщо в cache є profile під chat-key — повертаємо його."""
    chat_id = "cache_test_chat"
    stored = _make_farm(chat_id, crops=["соняшник"])
    _cache_set(_chat_fact_key(chat_id), stored)

    # patch _http_get_fact to ensure it's not called
    with patch("crews.agromatrix_crew.memory_manager._http_get_fact") as mock_get:
        result = load_farm_profile(chat_id, user_id="any_user")
        mock_get.assert_not_called()

    assert result["crops"] == ["соняшник"]


def test_load_farm_profile_falls_back_to_default():
    """При відсутності і в cache і в memory-service — default."""
    chat_id = "nodata_chat_999"
    with patch("crews.agromatrix_crew.memory_manager._http_get_fact", return_value=None):
        result = load_farm_profile(chat_id, user_id=None)
    assert result["_version"] == 5
    assert result["chat_id"] == chat_id


def test_load_farm_profile_no_user_id_no_crash():
    """load_farm_profile без user_id не падає."""
    chat_id = "chat_noid_888"
    with patch("crews.agromatrix_crew.memory_manager._http_get_fact", return_value=None):
        result = load_farm_profile(chat_id)  # user_id=None за замовчуванням
    assert result is not None
    assert result["chat_id"] == chat_id


# ─── Unit: lazy migration (legacy → chat key) ────────────────────────────────

def test_load_farm_profile_migrates_legacy():
    """
    load_farm_profile без chat-profile але з legacy → мігрує в chat-key.
    """
    chat_id = "migration_chat_1"
    user_id = "legacy_user_1"
    legacy_profile = _make_farm(user_id, crops=["ячмінь"])

    def mock_http_get(uid, key):
        if key == _chat_fact_key(chat_id):
            return None   # нового ключа нема
        if key == _legacy_farm_fact_key(user_id):
            return legacy_profile
        return None

    with patch("crews.agromatrix_crew.memory_manager._http_get_fact", side_effect=mock_http_get), \
         patch("crews.agromatrix_crew.memory_manager._http_upsert_fact", return_value=True):
        result = load_farm_profile(chat_id, user_id=user_id)

    assert result["crops"] == ["ячмінь"]
    assert result.get("chat_id") == chat_id
    assert "_migrated_from" in result
    assert user_id in result["_migrated_from"]


def test_load_farm_profile_migration_sets_chat_id():
    """Після міграції chat_id у профілі відповідає chat_id, а не user_id."""
    chat_id = "migration_chat_2"
    user_id = "legacy_user_2"
    legacy_profile = _make_farm(user_id)

    def mock_http_get(uid, key):
        return legacy_profile if key == _legacy_farm_fact_key(user_id) else None

    with patch("crews.agromatrix_crew.memory_manager._http_get_fact", side_effect=mock_http_get), \
         patch("crews.agromatrix_crew.memory_manager._http_upsert_fact", return_value=True):
        result = load_farm_profile(chat_id, user_id=user_id)

    assert result["chat_id"] == chat_id


# ─── Unit: migrate_farm_profile_legacy_to_chat ───────────────────────────────

def test_migrate_no_conflict_copies_profile():
    """Якщо chat-profile не існує → migration записує legacy."""
    chat_id = "mig_chat_3"
    user_id = "mig_user_3"
    legacy = _make_farm(user_id, crops=["соя"])

    with patch("crews.agromatrix_crew.memory_manager._http_get_fact", return_value=None), \
         patch("crews.agromatrix_crew.memory_manager._http_upsert_fact", return_value=True):
        result = migrate_farm_profile_legacy_to_chat(chat_id, user_id, legacy)

    assert result["crops"] == ["соя"]
    assert result["chat_id"] == chat_id


def test_migrate_conflict_keeps_existing():
    """
    Якщо chat-profile вже існує і відрізняється від legacy → не перезаписуємо,
    повертаємо існуючий.
    """
    chat_id = "mig_chat_4"
    user_id = "mig_user_4"
    existing = _make_farm(chat_id, crops=["пшениця"])
    legacy = _make_farm(user_id, crops=["кукурудза"])  # відрізняється

    with patch("crews.agromatrix_crew.memory_manager._http_get_fact", return_value=existing), \
         patch("crews.agromatrix_crew.memory_manager._http_upsert_fact") as mock_upsert:
        result = migrate_farm_profile_legacy_to_chat(chat_id, user_id, legacy)

    # Existing profile returned unchanged
    assert result["crops"] == ["пшениця"]
    # upsert NOT called (conflict — no overwrite)
    mock_upsert.assert_not_called()


def test_migrate_conflict_logs_telemetry(caplog):
    """Conflict → tlog виводить farm_profile_conflict."""
    import logging
    chat_id = "mig_chat_5"
    user_id = "mig_user_5"
    existing = _make_farm(chat_id, crops=["пшениця"])
    legacy = _make_farm(user_id, crops=["кукурудза"])

    with patch("crews.agromatrix_crew.memory_manager._http_get_fact", return_value=existing), \
         patch("crews.agromatrix_crew.memory_manager._http_upsert_fact", return_value=False), \
         caplog.at_level(logging.INFO, logger="crews.agromatrix_crew.memory_manager"):
        migrate_farm_profile_legacy_to_chat(chat_id, user_id, legacy)

    tagged = [r.getMessage() for r in caplog.records if TELEMETRY_TAG in r.getMessage()]
    assert any("farm_profile_conflict" in m for m in tagged), \
        f"Expected farm_profile_conflict telemetry, got: {tagged}"


def test_migrate_no_conflict_logs_migrated(caplog):
    """Successful migration → tlog виводить farm_profile_migrated."""
    import logging
    chat_id = "mig_chat_6"
    user_id = "mig_user_6"
    legacy = _make_farm(user_id, crops=["ріпак"])

    with patch("crews.agromatrix_crew.memory_manager._http_get_fact", return_value=None), \
         patch("crews.agromatrix_crew.memory_manager._http_upsert_fact", return_value=True), \
         caplog.at_level(logging.INFO, logger="crews.agromatrix_crew.memory_manager"):
        migrate_farm_profile_legacy_to_chat(chat_id, user_id, legacy)

    tagged = [r.getMessage() for r in caplog.records if TELEMETRY_TAG in r.getMessage()]
    assert any("farm_profile_migrated" in m for m in tagged), \
        f"Expected farm_profile_migrated telemetry, got: {tagged}"


# ─── Acceptance: two users share farm profile ────────────────────────────────

def test_two_users_same_chat_share_farm_profile():
    """
    userA і userB в одному chatX → ділять один FarmProfile.
    Перевірка: обидва load_farm_profile повертають одні й ті ж дані.
    """
    chat_id = "shared_chat_XY"
    user_a = "user_a_1"
    user_b = "user_b_2"

    shared_farm = _make_farm(chat_id, crops=["пшениця", "соняшник"])
    # chat-key вже заповнений (userA зберіг)
    _cache_set(_chat_fact_key(chat_id), shared_farm)

    # userA завантажує
    with patch("crews.agromatrix_crew.memory_manager._http_get_fact") as mock_get:
        farm_a = load_farm_profile(chat_id, user_id=user_a)
        mock_get.assert_not_called()  # cache hit

    # userB завантажує
    with patch("crews.agromatrix_crew.memory_manager._http_get_fact") as mock_get:
        farm_b = load_farm_profile(chat_id, user_id=user_b)
        mock_get.assert_not_called()  # cache hit

    assert farm_a["crops"] == farm_b["crops"]
    assert farm_a["chat_id"] == farm_b["chat_id"] == chat_id


def test_user_profile_per_user_independent():
    """UserProfile залишається per-user, не shared."""
    from crews.agromatrix_crew.memory_manager import _default_user_profile
    p_a = _default_user_profile("user_a_ind")
    p_b = _default_user_profile("user_b_ind")
    assert p_a["user_id"] != p_b["user_id"]


def test_farm_profile_update_visible_to_second_user():
    """
    Якщо userA записав farm_profile (crops оновились) →
    userB при наступному завантаженні бачить оновлені crops.
    """
    chat_id = "update_shared_chat"
    user_a = "user_upd_a"
    user_b = "user_upd_b"

    # Початковий стан
    initial_farm = _make_farm(chat_id, crops=["пшениця"])
    _cache_set(_chat_fact_key(chat_id), initial_farm)

    # userA додає crop
    updated_farm = deepcopy(initial_farm)
    updated_farm["crops"].append("кукурудза")
    _cache_set(_chat_fact_key(chat_id), updated_farm)

    # userB завантажує
    with patch("crews.agromatrix_crew.memory_manager._http_get_fact") as mock_get:
        farm_b = load_farm_profile(chat_id, user_id=user_b)
        mock_get.assert_not_called()

    assert "кукурудза" in farm_b["crops"]
    assert "пшениця" in farm_b["crops"]


# ─── backward-compat ──────────────────────────────────────────────────────────

def test_load_farm_backward_compat_no_user_id():
    """Старий виклик load_farm_profile(chat_id) без user_id — не падає."""
    with patch("crews.agromatrix_crew.memory_manager._http_get_fact", return_value=None):
        result = load_farm_profile("old_call_chat")
    assert result is not None
    assert result["_version"] == 5


def test_save_farm_uses_chat_key():
    """save_farm_profile зберігає під chat-key, не legacy."""
    chat_id = "save_test_chat"
    farm = _make_farm(chat_id)

    calls: list[tuple] = []

    def mock_upsert(uid, key, data):
        calls.append((uid, key))
        return True

    with patch("crews.agromatrix_crew.memory_manager._http_upsert_fact", side_effect=mock_upsert):
        save_farm_profile(chat_id, farm)

    assert calls, "upsert not called"
    used_key = calls[0][1]
    assert used_key == _chat_fact_key(chat_id), \
        f"Expected chat-key, got: {used_key!r}"
    assert "chat:" in used_key
    # Legacy key NOT used
    assert _legacy_farm_fact_key(chat_id) != used_key
