# 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 nonprofit and bookkeeping CRUD operations."""

from unittest.mock import patch

import pytest


@pytest.fixture
def tmp_data_dir(tmp_path):
    """Patch data dir to use temp directory."""
    with patch("familiar.skills.nonprofit.skill._get_data_dir", return_value=tmp_path):
        with patch("familiar.skills.bookkeeping.skill._get_data_dir", return_value=tmp_path):
            yield tmp_path


# ── Nonprofit Donor CRUD ──


class TestDonorCRUD:
    def test_create_donor(self, tmp_data_dir):
        from familiar.skills.nonprofit.skill import get_all_donors, log_donor

        log_donor({"name": "Jane Smith", "email": "jane@example.com", "phone": "555-1234"})
        donors = get_all_donors()
        assert len(donors) == 1
        assert donors[0]["name"] == "Jane Smith"
        assert donors[0]["email"] == "jane@example.com"

    def test_update_donor(self, tmp_data_dir):
        from familiar.skills.nonprofit.skill import get_all_donors, log_donor, update_donor

        log_donor({"name": "Jane Smith", "email": "old@example.com"})
        donors = get_all_donors()
        donor_id = donors[0]["id"]

        result = update_donor({"id": donor_id, "email": "new@example.com", "phone": "555-9999"})
        assert result["email"] == "new@example.com"
        assert result["phone"] == "555-9999"
        assert result["name"] == "Jane Smith"

    def test_update_donor_not_found(self, tmp_data_dir):
        from familiar.skills.nonprofit.skill import update_donor

        result = update_donor({"id": "nonexistent", "email": "x@x.com"})
        assert "error" in result

    def test_delete_donor(self, tmp_data_dir):
        from familiar.skills.nonprofit.skill import delete_donor, get_all_donors, log_donor

        log_donor({"name": "Jane Smith"})
        donors = get_all_donors()
        donor_id = donors[0]["id"]

        result = delete_donor(donor_id)
        assert result["success"] is True
        assert len(get_all_donors()) == 0

    def test_delete_donor_not_found(self, tmp_data_dir):
        from familiar.skills.nonprofit.skill import delete_donor

        result = delete_donor("nonexistent")
        assert "error" in result

    def test_add_interaction(self, tmp_data_dir):
        from familiar.skills.nonprofit.skill import add_interaction, get_all_donors, log_donor

        log_donor({"name": "Jane Smith"})
        donors = get_all_donors()
        donor_id = donors[0]["id"]

        result = add_interaction(
            {
                "donor_id": donor_id,
                "type": "gift",
                "amount": 500,
                "description": "Annual fund",
            }
        )
        assert result["total_given"] == 500
        # log_donor creates an initial interaction, so we expect 2 total
        assert len(result["interactions"]) == 2
        assert result["interactions"][-1]["type"] == "gift"

    def test_add_interaction_no_donor(self, tmp_data_dir):
        from familiar.skills.nonprofit.skill import add_interaction

        result = add_interaction({"donor_id": "bad_id", "type": "gift", "amount": 100})
        assert "error" in result


# ── AFP Donor Model (RFM, LYBUNT/SYBUNT) ──


class TestDonorAFPFields:
    def test_new_donor_has_afp_fields(self, tmp_data_dir):
        from familiar.skills.nonprofit.skill import get_all_donors, log_donor

        log_donor({"name": "Test Donor"})
        donors = get_all_donors()
        d = donors[0]
        assert "donor_level" in d
        assert "rfm_score" in d
        assert "lybunt" in d
        assert "sybunt" in d
        assert "do_not_solicit" in d
        assert "cultivation_stage" in d

    def test_rfm_computed_on_gift(self, tmp_data_dir):
        from familiar.skills.nonprofit.skill import add_interaction, get_all_donors, log_donor

        log_donor({"name": "Big Donor", "type": "gift", "amount": 5000})
        donor_id = get_all_donors()[0]["id"]

        # Add another gift
        result = add_interaction(
            {
                "donor_id": donor_id,
                "type": "gift",
                "amount": 5000,
            }
        )
        assert result["rfm_score"] > 0
        assert result["recency_score"] == 5  # Just gave
        assert result["monetary_score"] >= 4  # $5000 avg
        assert result["donor_level"] == "major"  # $10k total
        assert result["gift_count"] == 2
        assert result["avg_gift"] == 5000
        assert result["largest_gift"] == 5000

    def test_rfm_small_donor(self, tmp_data_dir):
        from familiar.skills.nonprofit.skill import get_all_donors, log_donor

        log_donor({"name": "Small Donor", "type": "gift", "amount": 25})
        d = get_all_donors()[0]
        assert d["donor_level"] == "small"
        assert d["monetary_score"] == 1  # <$50

    def test_lybunt_sybunt_flags(self, tmp_data_dir):
        from familiar.skills.nonprofit.skill import (
            _compute_lybunt_sybunt,
            get_all_donors,
            log_donor,
        )

        log_donor({"name": "Lapsed Donor"})
        d = get_all_donors()[0]

        # Simulate: gave last year but not this year
        from datetime import datetime

        last_year = datetime.now().year - 1
        d["interactions"] = [
            {"date": f"{last_year}-06-15T00:00:00", "type": "gift", "amount": 100},
        ]
        _compute_lybunt_sybunt(d)
        assert d["lybunt"] is True
        assert d["sybunt"] is False

    def test_update_donor_afp_fields(self, tmp_data_dir):
        from familiar.skills.nonprofit.skill import get_all_donors, log_donor, update_donor

        log_donor({"name": "Jane"})
        donor_id = get_all_donors()[0]["id"]

        result = update_donor(
            {
                "id": donor_id,
                "cultivation_stage": "soliciting",
                "communication_pref": "email",
                "do_not_call": True,
                "employer_match": True,
            }
        )
        assert result["cultivation_stage"] == "soliciting"
        assert result["communication_pref"] == "email"
        assert result["do_not_call"] is True
        assert result["employer_match"] is True

    def test_backfill_legacy_donor(self, tmp_data_dir):
        """Legacy donors without AFP fields get backfilled on load."""
        import json

        # Write a legacy donor directly (no AFP fields)
        data = {
            "donors": [
                {
                    "id": "legacy1",
                    "name": "Old Donor",
                    "email": "",
                    "phone": "",
                    "total_given": 500,
                    "first_gift": "2025-01-01T00:00:00",
                    "last_gift": "2025-06-01T00:00:00",
                    "interactions": [],
                    "notes": "",
                    "tags": [],
                }
            ],
            "grants": [],
            "notes": [],
            "compliance": [],
        }
        with open(tmp_data_dir / "nonprofit.json", "w") as f:
            json.dump(data, f)

        from familiar.skills.nonprofit.skill import get_all_donors

        donors = get_all_donors()
        d = donors[0]
        assert d["donor_level"] == ""  # Backfilled default
        assert d["rfm_score"] == 0
        assert d["lybunt"] is False
        assert d["do_not_solicit"] is False


# ── Enhanced Grant Model ──


class TestGrantEnhancedFields:
    def test_new_grant_has_enhanced_fields(self, tmp_data_dir):
        from familiar.skills.nonprofit.skill import add_grant, get_all_grants

        add_grant(
            {
                "name": "Federal Grant",
                "funder": "HHS",
                "cfda_number": "93.778",
                "grant_type": "federal",
                "budget_total": 100000,
                "indirect_rate": 10,
            }
        )
        grants = get_all_grants()
        g = grants[0]
        assert g["cfda_number"] == "93.778"
        assert g["grant_type"] == "federal"
        assert g["budget_total"] == 100000
        assert g["indirect_rate"] == 10
        assert g["reports"] == []

    def test_update_grant_enhanced_fields(self, tmp_data_dir):
        from familiar.skills.nonprofit.skill import add_grant, get_all_grants, update_grant

        add_grant({"name": "Test Grant"})
        grant_id = get_all_grants()[0]["id"]

        result = update_grant(
            {
                "id": grant_id,
                "award_number": "AWD-2026-001",
                "program_officer": "Dr. Smith",
                "program_officer_email": "smith@hhs.gov",
                "budget_spent": 25000,
                "match_required": 10000,
                "match_secured": 5000,
            }
        )
        assert result["award_number"] == "AWD-2026-001"
        assert result["program_officer"] == "Dr. Smith"
        assert result["budget_spent"] == 25000
        assert result["match_secured"] == 5000

    def test_backfill_legacy_grant(self, tmp_data_dir):
        """Legacy grants without enhanced fields get backfilled on load."""
        import json

        data = {
            "donors": [],
            "grants": [
                {
                    "id": "leg1",
                    "name": "Old Grant",
                    "funder": "Foundation",
                    "amount": 50000,
                    "status": "awarded",
                    "deadline": None,
                    "report_due": None,
                    "start_date": None,
                    "end_date": None,
                    "notes": "",
                    "created_at": "2025-01-01T00:00:00",
                }
            ],
            "notes": [],
            "compliance": [],
        }
        with open(tmp_data_dir / "nonprofit.json", "w") as f:
            json.dump(data, f)

        from familiar.skills.nonprofit.skill import get_all_grants

        grants = get_all_grants()
        g = grants[0]
        assert g["cfda_number"] == ""
        assert g["budget_total"] == 0
        assert g["reports"] == []
        assert g["program_officer"] == ""


# ── Grant CRUD ──


class TestGrantCRUD:
    def test_create_grant(self, tmp_data_dir):
        from familiar.skills.nonprofit.skill import add_grant, get_all_grants

        add_grant({"name": "Community Grant", "funder": "Smith Foundation", "amount": 50000})
        grants = get_all_grants()
        assert len(grants) == 1
        assert grants[0]["name"] == "Community Grant"

    def test_update_grant(self, tmp_data_dir):
        from familiar.skills.nonprofit.skill import add_grant, get_all_grants, update_grant

        add_grant({"name": "Community Grant", "status": "prospect"})
        grants = get_all_grants()
        grant_id = grants[0]["id"]

        result = update_grant({"id": grant_id, "status": "applied", "amount": 75000})
        assert result["status"] == "applied"
        assert result["amount"] == 75000

    def test_update_grant_not_found(self, tmp_data_dir):
        from familiar.skills.nonprofit.skill import update_grant

        result = update_grant({"id": "nonexistent", "status": "awarded"})
        assert "error" in result

    def test_delete_grant(self, tmp_data_dir):
        from familiar.skills.nonprofit.skill import add_grant, delete_grant, get_all_grants

        add_grant({"name": "Test Grant"})
        grants = get_all_grants()
        grant_id = grants[0]["id"]

        result = delete_grant(grant_id)
        assert result["success"] is True
        assert len(get_all_grants()) == 0


# ── Note CRUD ──


class TestNoteCRUD:
    def test_create_note(self, tmp_data_dir):
        from familiar.skills.nonprofit.skill import add_note, get_all_notes

        add_note({"content": "Board meeting at 3pm", "category": "board"})
        notes = get_all_notes()
        assert len(notes) == 1
        assert notes[0]["content"] == "Board meeting at 3pm"
        assert notes[0]["category"] == "board"

    def test_delete_note(self, tmp_data_dir):
        from familiar.skills.nonprofit.skill import add_note, delete_note, get_all_notes

        add_note({"content": "Test note"})
        notes = get_all_notes()
        note_id = notes[0]["id"]

        result = delete_note(note_id)
        assert result["success"] is True
        assert len(get_all_notes()) == 0


# ── Bookkeeping CRUD ──


class TestBookkeepingCRUD:
    def test_log_income(self, tmp_data_dir):
        from familiar.skills.bookkeeping.skill import get_all_transactions, log_income

        log_income({"amount": 1000, "source": "Jane Smith", "category": "individual_donations"})
        txns = get_all_transactions()
        assert len(txns) == 1
        assert txns[0]["type"] == "income"
        assert txns[0]["amount"] == 1000

    def test_log_expense(self, tmp_data_dir):
        from familiar.skills.bookkeeping.skill import get_all_transactions, log_expense

        log_expense({"amount": 250, "vendor": "Office Depot", "category": "supplies"})
        txns = get_all_transactions()
        assert len(txns) == 1
        assert txns[0]["type"] == "expense"
        assert txns[0]["vendor"] == "Office Depot"

    def test_update_transaction(self, tmp_data_dir):
        from familiar.skills.bookkeeping.skill import (
            get_all_transactions,
            log_income,
            update_transaction,
        )

        log_income({"amount": 1000, "category": "individual_donations"})
        txns = get_all_transactions()
        tx_id = txns[0]["id"]

        result = update_transaction({"id": tx_id, "amount": 1500, "description": "Updated"})
        assert result["amount"] == 1500
        assert result["description"] == "Updated"

    def test_delete_transaction(self, tmp_data_dir):
        from familiar.skills.bookkeeping.skill import (
            delete_transaction,
            get_all_transactions,
            log_income,
        )

        log_income({"amount": 500})
        txns = get_all_transactions()
        tx_id = txns[0]["id"]

        result = delete_transaction(tx_id)
        assert result["success"] is True
        assert len(get_all_transactions()) == 0

    def test_filter_transactions(self, tmp_data_dir):
        from familiar.skills.bookkeeping.skill import get_all_transactions, log_expense, log_income

        log_income({"amount": 1000, "category": "individual_donations", "date": "2026-01-15"})
        log_expense({"amount": 200, "category": "supplies", "date": "2026-02-01"})

        # Filter by type
        income = get_all_transactions({"type": "income"})
        assert len(income) == 1
        assert income[0]["type"] == "income"

        # Filter by date range
        jan = get_all_transactions({"start_date": "2026-01-01", "end_date": "2026-01-31"})
        assert len(jan) == 1

    def test_get_categories(self, tmp_data_dir):
        from familiar.skills.bookkeeping.skill import get_categories

        cats = get_categories()
        assert "income" in cats
        assert "expense" in cats
        assert "individual_donations" in cats["income"]

    def test_get_funds(self, tmp_data_dir):
        from familiar.skills.bookkeeping.skill import get_funds

        funds = get_funds()
        assert len(funds) >= 1
        assert funds[0]["name"] == "General"
