# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""Tests for Stewardship Engine and Batch Communications."""

from unittest import mock

import pytest


@pytest.fixture(autouse=True)
def _patch_data_dir(tmp_path):
    with (
        mock.patch("familiar.skills.nonprofit.stewardship._data_dir", return_value=tmp_path),
        mock.patch("familiar.skills.nonprofit.batch_comms._data_dir", return_value=tmp_path),
    ):
        yield


# ── Stewardship Matrix Tests ─────────────────────────────────────────


class TestStewardshipMatrix:
    def test_small_gift(self):
        from familiar.skills.nonprofit.stewardship import _get_actions_for_gift

        actions = _get_actions_for_gift(50)
        types = [a[0] for a in actions]
        assert "thank_you_email" in types
        assert "phone_call" not in types

    def test_medium_gift(self):
        from familiar.skills.nonprofit.stewardship import _get_actions_for_gift

        actions = _get_actions_for_gift(250)
        types = [a[0] for a in actions]
        assert "thank_you_email" in types
        assert "phone_call" in types
        assert "thank_you_letter" not in types

    def test_large_gift(self):
        from familiar.skills.nonprofit.stewardship import _get_actions_for_gift

        actions = _get_actions_for_gift(5000)
        types = [a[0] for a in actions]
        assert "thank_you_email" in types
        assert "thank_you_letter" in types
        assert "phone_call" in types

    def test_major_gift(self):
        from familiar.skills.nonprofit.stewardship import _get_actions_for_gift

        actions = _get_actions_for_gift(15000)
        types = [a[0] for a in actions]
        assert "recognition" in types
        assert "impact_report" in types

    def test_first_time_donor(self):
        from familiar.skills.nonprofit.stewardship import _get_actions_for_gift

        actions = _get_actions_for_gift(50, is_first_time=True)
        types = [a[0] for a in actions]
        assert "welcome_packet" in types

    def test_first_time_not_added_for_returning(self):
        from familiar.skills.nonprofit.stewardship import _get_actions_for_gift

        actions = _get_actions_for_gift(50, is_first_time=False)
        types = [a[0] for a in actions]
        assert "welcome_packet" not in types


# ── StewardshipStore Tests ────────────────────────────────────────────


class TestStewardshipStore:
    def test_create_stewardship(self):
        from familiar.skills.nonprofit.stewardship import get_stewardship_store

        store = get_stewardship_store()
        sid = store.create_stewardship(
            donor_id="d_001",
            donor_name="Jane Doe",
            trigger_amount=500,
            trigger_type="gift_detected",
        )
        assert sid.startswith("stw_")
        entry = store.get_entry(sid)
        assert entry["donor_name"] == "Jane Doe"
        assert entry["trigger_amount"] == 500
        assert len(entry["actions"]) > 0

    def test_duplicate_same_day(self):
        from familiar.skills.nonprofit.stewardship import get_stewardship_store

        store = get_stewardship_store()
        store.create_stewardship(donor_id="d_001", donor_name="Jane", trigger_type="gift_detected")
        result = store.create_stewardship(
            donor_id="d_001", donor_name="Jane", trigger_type="gift_detected"
        )
        assert result is None

    def test_complete_action(self):
        from familiar.skills.nonprofit.stewardship import get_stewardship_store

        store = get_stewardship_store()
        sid = store.create_stewardship(donor_id="d_001", donor_name="Jane", trigger_amount=100)
        assert store.complete_action(sid, 0) is True
        entry = store.get_entry(sid)
        assert entry["actions"][0]["status"] == "completed"
        assert entry["actions"][0]["completed_at"] is not None

    def test_skip_action(self):
        from familiar.skills.nonprofit.stewardship import get_stewardship_store

        store = get_stewardship_store()
        sid = store.create_stewardship(donor_id="d_001", donor_name="Jane", trigger_amount=50)
        assert store.skip_action(sid, 0) is True
        entry = store.get_entry(sid)
        assert entry["actions"][0]["status"] == "skipped"

    def test_get_for_donor(self):
        from familiar.skills.nonprofit.stewardship import get_stewardship_store

        store = get_stewardship_store()
        store.create_stewardship(donor_id="d_001", donor_name="Jane", trigger_type="gift_detected")
        store.create_stewardship(donor_id="d_002", donor_name="Bob", trigger_type="gift_detected")
        history = store.get_for_donor("d_001")
        assert len(history) == 1

    def test_due_dates_computed_correctly(self):
        from familiar.skills.nonprofit.stewardship import get_stewardship_store

        store = get_stewardship_store()
        sid = store.create_stewardship(donor_id="d_001", donor_name="Jane", trigger_amount=50)
        entry = store.get_entry(sid)
        # Small gift = thank_you_email due in 2 days
        action = entry["actions"][0]
        assert action["type"] == "thank_you_email"
        # Due date should be 2 days from now
        from datetime import datetime, timedelta, timezone

        expected = (datetime.now(timezone.utc) + timedelta(days=2)).strftime("%Y-%m-%d")
        assert action["due_date"] == expected

    def test_completion_rate(self):
        from familiar.skills.nonprofit.stewardship import get_stewardship_store

        store = get_stewardship_store()
        sid = store.create_stewardship(donor_id="d_001", donor_name="Jane", trigger_amount=150)
        # 150 = 100+ tier: 2 actions (email, phone)
        entry = store.get_entry(sid)
        assert len(entry["actions"]) == 2
        store.complete_action(sid, 0)
        rate = store.completion_rate()
        assert rate == 50.0

    def test_persistence(self):
        from familiar.skills.nonprofit.stewardship import get_stewardship_store

        store1 = get_stewardship_store()
        store1.create_stewardship(donor_id="d_001", donor_name="Jane")
        store2 = get_stewardship_store()
        assert len(store2.get_for_donor("d_001")) == 1


# ── Batch Communications Tests ────────────────────────────────────────


class TestBatchStore:
    def test_create_batch(self):
        from familiar.skills.nonprofit.batch_comms import get_batch_store

        store = get_batch_store()
        bid = store.create_batch(name="Annual Appeal 2026", channel="email")
        assert bid.startswith("batch_")
        batch = store.get_batch(bid)
        assert batch["name"] == "Annual Appeal 2026"
        assert batch["status"] == "draft"

    def test_list_batches(self):
        from familiar.skills.nonprofit.batch_comms import get_batch_store

        store = get_batch_store()
        store.create_batch(name="Batch A")
        store.create_batch(name="Batch B")
        batches = store.list_batches()
        assert len(batches) == 2

    def test_update_status(self):
        from familiar.skills.nonprofit.batch_comms import get_batch_store

        store = get_batch_store()
        bid = store.create_batch(name="Test")
        assert store.update_status(bid, "ready") is True
        batch = store.get_batch(bid)
        assert batch["status"] == "ready"

    def test_record_generation(self):
        from familiar.skills.nonprofit.batch_comms import get_batch_store

        store = get_batch_store()
        bid = store.create_batch(name="Test")
        store.record_generation(bid, "d_001", "Dear Jane, thank you...")
        batch = store.get_batch(bid)
        assert batch["generated_count"] == 1
        assert batch["items"][0]["donor_id"] == "d_001"

    def test_record_send_success(self):
        from familiar.skills.nonprofit.batch_comms import get_batch_store

        store = get_batch_store()
        bid = store.create_batch(name="Test")
        store.record_generation(bid, "d_001", "content")
        store.record_send(bid, "d_001", success=True)
        batch = store.get_batch(bid)
        assert batch["sent_count"] == 1
        assert batch["items"][0]["status"] == "sent"

    def test_record_send_failure(self):
        from familiar.skills.nonprofit.batch_comms import get_batch_store

        store = get_batch_store()
        bid = store.create_batch(name="Test")
        store.record_generation(bid, "d_001", "content")
        store.record_send(bid, "d_001", success=False, error="SMTP error")
        batch = store.get_batch(bid)
        assert batch["failed_count"] == 1
        assert batch["items"][0]["error"] == "SMTP error"

    def test_cancel_batch(self):
        from familiar.skills.nonprofit.batch_comms import get_batch_store

        store = get_batch_store()
        bid = store.create_batch(name="Cancel Me")
        assert store.cancel_batch(bid) is True
        assert store.get_batch(bid)["status"] == "cancelled"

    def test_persistence(self):
        from familiar.skills.nonprofit.batch_comms import get_batch_store

        store1 = get_batch_store()
        store1.create_batch(name="Persist")
        store2 = get_batch_store()
        assert len(store2.list_batches()) == 1


class TestSegmentation:
    def test_matches_criteria_level(self):
        from familiar.skills.nonprofit.batch_comms import _matches_criteria

        donor = {"donor_level": "major", "total_given": 50000}
        assert _matches_criteria(donor, {"level": "major"}) is True
        assert _matches_criteria(donor, {"level": "minor"}) is False

    def test_matches_criteria_rfm_range(self):
        from familiar.skills.nonprofit.batch_comms import _matches_criteria

        donor = {"rfm_score": 7}
        assert _matches_criteria(donor, {"rfm_min": 5, "rfm_max": 10}) is True
        assert _matches_criteria(donor, {"rfm_min": 8}) is False

    def test_matches_criteria_lybunt(self):
        from familiar.skills.nonprofit.batch_comms import _matches_criteria

        donor = {"lybunt": True}
        assert _matches_criteria(donor, {"lybunt": True}) is True
        donor2 = {"lybunt": False}
        assert _matches_criteria(donor2, {"lybunt": True}) is False

    def test_matches_criteria_min_total(self):
        from familiar.skills.nonprofit.batch_comms import _matches_criteria

        donor = {"total_given": 500}
        assert _matches_criteria(donor, {"min_total": 100}) is True
        assert _matches_criteria(donor, {"min_total": 1000}) is False

    def test_matches_criteria_tags(self):
        from familiar.skills.nonprofit.batch_comms import _matches_criteria

        donor = {"tags": ["recurring", "gala_2025"]}
        assert _matches_criteria(donor, {"tags": ["recurring"]}) is True
        assert _matches_criteria(donor, {"tags": ["never_tagged"]}) is False

    def test_matches_criteria_gift_count(self):
        from familiar.skills.nonprofit.batch_comms import _matches_criteria

        donor = {"interactions": [{"type": "gift"}]}
        assert _matches_criteria(donor, {"gift_count_max": 1}) is True
        assert _matches_criteria(donor, {"gift_count_max": 0}) is False

    def test_predefined_segments(self):
        from familiar.skills.nonprofit.batch_comms import PREDEFINED_SEGMENTS

        assert "annual_appeal" in PREDEFINED_SEGMENTS
        assert "lybunt_reactivation" in PREDEFINED_SEGMENTS
        assert "major_donor_update" in PREDEFINED_SEGMENTS
        assert "first_time_welcome" in PREDEFINED_SEGMENTS
