# 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 Sprint 6 skills: Grants.gov, ProPublica, CiviCRM, GlobalGiving, MailerLite."""

from unittest.mock import MagicMock, patch

HTTPX_GET = "familiar.skills._api_utils.httpx.get"
HTTPX_POST = "familiar.skills._api_utils.httpx.post"
CACHE_GET = "familiar.core.api_cache.get_cached"
CACHE_SET = "familiar.core.api_cache.set_cached"

CIVICRM_ENV = {
    "CIVICRM_URL": "https://crm.example.org",
    "CIVICRM_API_KEY": "apikey123",
    "CIVICRM_SITE_KEY": "sitekey456",
}
GLOBALGIVING_ENV = {"GLOBALGIVING_API_KEY": "ggkey"}
MAILERLITE_ENV = {"MAILERLITE_API_KEY": "mlkey"}


def _mock_response(data, status=200):
    resp = MagicMock()
    resp.status_code = status
    resp.json.return_value = data
    resp.content = b'{"ok": true}'
    resp.text = '{"ok": true}'
    return resp


def _civicrm_env(key):
    return CIVICRM_ENV.get(key, "")


def _gg_env(key):
    return GLOBALGIVING_ENV.get(key, "")


def _ml_env(key):
    return MAILERLITE_ENV.get(key, "")


# ── Grants.gov ──────────────────────────────────────────────────────────


class TestSearchGrants:
    def test_no_query(self):
        from familiar.skills.grants_gov.skill import search_grants

        result = search_grants({})
        assert "provide a keyword" in result.lower()

    def test_success(self):
        from familiar.skills.grants_gov.skill import search_grants

        resp = _mock_response(
            {
                "data": {
                    "totalCount": 5,
                    "oppHits": [
                        {
                            "title": "Community Health Initiative",
                            "number": "HHS-2026-001",
                            "agency": "HHS",
                            "closeDate": "2026-06-15",
                            "awardFloor": 50000,
                            "awardCeiling": 500000,
                        },
                    ],
                }
            }
        )

        with (
            patch(HTTPX_POST, return_value=resp),
        ):
            result = search_grants({"keyword": "health"})
        assert "Community Health Initiative" in result
        assert "HHS" in result
        assert "2026-06-15" in result
        assert "$500,000" in result


class TestGrantDetails:
    def test_no_id(self):
        from familiar.skills.grants_gov.skill import grant_details

        result = grant_details({"opportunity_id": ""})
        assert "provide an opportunity_id" in result.lower()

    def test_success(self):
        from familiar.skills.grants_gov.skill import grant_details

        resp = _mock_response(
            {
                "data": {
                    "title": "Education Equity Grant",
                    "number": "ED-2026-005",
                    "agency": "Department of Education",
                    "closeDate": "2026-07-01",
                    "awardCeiling": 250000,
                    "description": "Supports schools in underserved communities.",
                }
            }
        )

        with (
            patch(HTTPX_GET, return_value=resp),
            patch(CACHE_GET, return_value=None),
            patch(CACHE_SET),
        ):
            result = grant_details({"opportunity_id": "12345"})
        assert "Education Equity Grant" in result
        assert "Department of Education" in result


class TestGrantDeadlines:
    def test_success(self):
        from familiar.skills.grants_gov.skill import grant_deadlines

        resp = _mock_response(
            {
                "data": {
                    "oppHits": [
                        {
                            "title": "USDA Rural Development",
                            "closeDate": "2026-04-01",
                            "agency": "USDA",
                            "awardCeiling": 100000,
                        },
                    ],
                }
            }
        )

        with (
            patch(HTTPX_POST, return_value=resp),
        ):
            result = grant_deadlines({})
        assert "USDA Rural Development" in result
        assert "2026-04-01" in result


class TestGrantsGovTools:
    def test_tools_registered(self):
        from familiar.skills.grants_gov.skill import TOOLS

        names = [t["name"] for t in TOOLS]
        assert "search_grants" in names
        assert "grant_details" in names
        assert "grant_deadlines" in names


# ── ProPublica Nonprofit Explorer ───────────────────────────────────────


class TestNonprofitSearch:
    def test_no_query(self):
        from familiar.skills.propublica_nonprofit.skill import nonprofit_search

        result = nonprofit_search({"query": ""})
        assert "provide a search query" in result.lower()

    def test_success(self):
        from familiar.skills.propublica_nonprofit.skill import nonprofit_search

        resp = _mock_response(
            {
                "total_results": 3,
                "organizations": [
                    {
                        "name": "Portland Food Bank",
                        "ein": "931234567",
                        "city": "Portland",
                        "state": "OR",
                        "ntee_code": "K31",
                        "income_amount": 5000000,
                        "asset_amount": 2000000,
                    },
                ],
            }
        )

        with (
            patch(HTTPX_GET, return_value=resp),
            patch(CACHE_GET, return_value=None),
            patch(CACHE_SET),
        ):
            result = nonprofit_search({"query": "Portland Food Bank"})
        assert "Portland Food Bank" in result
        assert "931234567" in result
        assert "$5,000,000" in result


class TestNonprofit990:
    def test_invalid_ein(self):
        from familiar.skills.propublica_nonprofit.skill import nonprofit_990

        result = nonprofit_990({"ein": "abc"})
        assert "invalid ein" in result.lower()

    def test_success(self):
        from familiar.skills.propublica_nonprofit.skill import nonprofit_990

        resp = _mock_response(
            {
                "organization": {
                    "name": "Test Nonprofit",
                    "city": "New York",
                    "state": "NY",
                    "ntee_code": "P20",
                    "subsection_code": "3",
                },
                "filings_with_data": [
                    {
                        "tax_prd_yr": "2024",
                        "totrevenue": 1500000,
                        "totfuncexpns": 1200000,
                        "totassetsend": 800000,
                        "pdf_url": "https://example.com/990.pdf",
                    },
                ],
            }
        )

        with (
            patch(HTTPX_GET, return_value=resp),
            patch(CACHE_GET, return_value=None),
            patch(CACHE_SET),
        ):
            result = nonprofit_990({"ein": "131624100"})
        assert "Test Nonprofit" in result
        assert "$1,500,000" in result
        assert "501(c)(3)" in result


class TestCompareNonprofits:
    def test_missing_ein(self):
        from familiar.skills.propublica_nonprofit.skill import compare_nonprofits

        result = compare_nonprofits({"ein1": "123456789", "ein2": ""})
        assert "provide two eins" in result.lower()

    def test_success(self):
        from familiar.skills.propublica_nonprofit.skill import compare_nonprofits

        resp1 = _mock_response(
            {
                "organization": {"name": "Org A", "city": "NY", "state": "NY", "ntee_code": "P"},
                "filings_with_data": [
                    {"totrevenue": 1000000, "totfuncexpns": 800000, "totassetsend": 500000}
                ],
            }
        )
        resp2 = _mock_response(
            {
                "organization": {"name": "Org B", "city": "LA", "state": "CA", "ntee_code": "K"},
                "filings_with_data": [
                    {"totrevenue": 2000000, "totfuncexpns": 1500000, "totassetsend": 900000}
                ],
            }
        )

        call_count = {"n": 0}

        def _get_side_effect(*args, **kwargs):
            call_count["n"] += 1
            return resp1 if call_count["n"] <= 1 else resp2

        with (
            patch(HTTPX_GET, side_effect=_get_side_effect),
            patch(CACHE_GET, return_value=None),
            patch(CACHE_SET),
        ):
            result = compare_nonprofits({"ein1": "111111111", "ein2": "222222222"})
        assert "Org A" in result
        assert "Org B" in result
        assert "Revenue" in result


class TestProPublicaTools:
    def test_tools_registered(self):
        from familiar.skills.propublica_nonprofit.skill import TOOLS

        names = [t["name"] for t in TOOLS]
        assert "nonprofit_search" in names
        assert "nonprofit_990" in names
        assert "compare_nonprofits" in names


# ── CiviCRM ─────────────────────────────────────────────────────────────


class TestCrmContacts:
    def test_no_config(self):
        from familiar.skills.civicrm.skill import crm_contacts

        with patch("familiar.skills.civicrm.skill.get_env", return_value=""):
            result = crm_contacts({})
        assert "not configured" in result.lower()

    def test_success(self):
        from familiar.skills.civicrm.skill import crm_contacts

        resp = _mock_response(
            {
                "count": 2,
                "values": [
                    {
                        "id": "1",
                        "display_name": "Jane Donor",
                        "email": "jane@example.com",
                        "phone": "555-1234",
                    },
                ],
            }
        )

        with (
            patch("familiar.skills.civicrm.skill.get_env", side_effect=_civicrm_env),
            patch(HTTPX_GET, return_value=resp),
            patch(CACHE_GET, return_value=None),
            patch(CACHE_SET),
        ):
            result = crm_contacts({})
        assert "Jane Donor" in result
        assert "jane@example.com" in result


class TestCrmDonations:
    def test_success(self):
        from familiar.skills.civicrm.skill import crm_donations

        resp = _mock_response(
            {
                "values": [
                    {
                        "total_amount": "500.00",
                        "currency": "USD",
                        "receive_date": "2026-03-01 10:00:00",
                        "contribution_status": "Completed",
                        "source": "Online",
                        "sort_name": "Doe, Jane",
                    },
                ],
            }
        )

        with (
            patch("familiar.skills.civicrm.skill.get_env", side_effect=_civicrm_env),
            patch(HTTPX_GET, return_value=resp),
            patch(CACHE_GET, return_value=None),
            patch(CACHE_SET),
        ):
            result = crm_donations({})
        assert "$500.00" in result
        assert "Doe, Jane" in result


class TestCrmAddDonation:
    def test_no_contact(self):
        from familiar.skills.civicrm.skill import crm_add_donation

        with patch("familiar.skills.civicrm.skill.get_env", side_effect=_civicrm_env):
            result = crm_add_donation({"contact_id": "", "amount": 100})
        assert "provide a contact_id" in result.lower()

    def test_success(self):
        from familiar.skills.civicrm.skill import crm_add_donation

        resp = _mock_response(
            {
                "is_error": 0,
                "values": {"1": {"id": "999"}},
            }
        )

        with (
            patch("familiar.skills.civicrm.skill.get_env", side_effect=_civicrm_env),
            patch(HTTPX_GET, return_value=resp),
        ):
            result = crm_add_donation({"contact_id": "42", "amount": 250, "source": "Gala"})
        assert "donation logged" in result.lower()
        assert "999" in result


class TestCrmEvents:
    def test_success(self):
        from familiar.skills.civicrm.skill import crm_events

        resp = _mock_response(
            {
                "values": [
                    {
                        "title": "Annual Gala",
                        "start_date": "2026-06-15 18:00:00",
                        "end_date": "2026-06-15 22:00:00",
                        "event_type": "Fundraiser",
                        "is_active": "1",
                    },
                ],
            }
        )

        with (
            patch("familiar.skills.civicrm.skill.get_env", side_effect=_civicrm_env),
            patch(HTTPX_GET, return_value=resp),
            patch(CACHE_GET, return_value=None),
            patch(CACHE_SET),
        ):
            result = crm_events({})
        assert "Annual Gala" in result
        assert "2026-06-15" in result


class TestCiviCRMTools:
    def test_tools_registered(self):
        from familiar.skills.civicrm.skill import TOOLS

        names = [t["name"] for t in TOOLS]
        assert "crm_contacts" in names
        assert "crm_donations" in names
        assert "crm_add_donation" in names
        assert "crm_events" in names


# ── GlobalGiving ────────────────────────────────────────────────────────


class TestSearchProjects:
    def test_no_config(self):
        from familiar.skills.globalgiving.skill import search_projects

        with patch("familiar.skills.globalgiving.skill.get_env", return_value=""):
            result = search_projects({"keyword": "education"})
        assert "not configured" in result.lower()

    def test_no_query(self):
        from familiar.skills.globalgiving.skill import search_projects

        with patch("familiar.skills.globalgiving.skill.get_env", side_effect=_gg_env):
            result = search_projects({})
        assert "provide a keyword" in result.lower()

    def test_success(self):
        from familiar.skills.globalgiving.skill import search_projects

        resp = _mock_response(
            {
                "projects": {
                    "project": [
                        {
                            "id": "12345",
                            "title": "Clean Water for Kenya",
                            "organization": {"name": "Water NGO"},
                            "country": "Kenya",
                            "themeName": "clean water",
                            "funding": 15000,
                            "goal": 25000,
                        },
                    ],
                }
            }
        )

        with (
            patch("familiar.skills.globalgiving.skill.get_env", side_effect=_gg_env),
            patch(HTTPX_GET, return_value=resp),
            patch(CACHE_GET, return_value=None),
            patch(CACHE_SET),
        ):
            result = search_projects({"keyword": "water"})
        assert "Clean Water for Kenya" in result
        assert "Water NGO" in result
        assert "60%" in result


class TestProjectDetails:
    def test_no_id(self):
        from familiar.skills.globalgiving.skill import project_details

        with patch("familiar.skills.globalgiving.skill.get_env", side_effect=_gg_env):
            result = project_details({"project_id": ""})
        assert "provide a project_id" in result.lower()

    def test_success(self):
        from familiar.skills.globalgiving.skill import project_details

        resp = _mock_response(
            {
                "project": {
                    "title": "Solar Schools",
                    "organization": {"name": "Green Energy NGO"},
                    "country": "India",
                    "themeName": "climate",
                    "funding": 8000,
                    "goal": 20000,
                    "numberOfDonations": 45,
                    "summary": "Installing solar panels in rural schools.",
                }
            }
        )

        with (
            patch("familiar.skills.globalgiving.skill.get_env", side_effect=_gg_env),
            patch(HTTPX_GET, return_value=resp),
            patch(CACHE_GET, return_value=None),
            patch(CACHE_SET),
        ):
            result = project_details({"project_id": "67890"})
        assert "Solar Schools" in result
        assert "India" in result
        assert "45" in result


class TestGlobalGivingTools:
    def test_tools_registered(self):
        from familiar.skills.globalgiving.skill import TOOLS

        names = [t["name"] for t in TOOLS]
        assert "search_projects" in names
        assert "project_details" in names


# ── MailerLite ──────────────────────────────────────────────────────────


class TestListSubscribers:
    def test_no_config(self):
        from familiar.skills.mailerlite.skill import list_subscribers

        with patch("familiar.skills.mailerlite.skill.get_env", return_value=""):
            result = list_subscribers({})
        assert "not configured" in result.lower()

    def test_success(self):
        from familiar.skills.mailerlite.skill import list_subscribers

        resp = _mock_response(
            {
                "data": [
                    {
                        "email": "donor@example.com",
                        "fields": {"name": "Jane Doe"},
                        "status": "active",
                        "created_at": "2026-01-15T10:00:00Z",
                        "stats": {"opens_count": 12, "clicks_count": 3},
                    },
                ],
                "meta": {"total": 150},
            }
        )

        with (
            patch("familiar.skills.mailerlite.skill.get_env", side_effect=_ml_env),
            patch(HTTPX_GET, return_value=resp),
            patch(CACHE_GET, return_value=None),
            patch(CACHE_SET),
        ):
            result = list_subscribers({})
        assert "donor@example.com" in result
        assert "Jane Doe" in result
        assert "12 opens" in result


class TestAddSubscriber:
    def test_no_email(self):
        from familiar.skills.mailerlite.skill import add_subscriber

        with patch("familiar.skills.mailerlite.skill.get_env", side_effect=_ml_env):
            result = add_subscriber({"email": ""})
        assert "provide an email" in result.lower()

    def test_success(self):
        from familiar.skills.mailerlite.skill import add_subscriber

        resp = _mock_response(
            {
                "data": {
                    "id": "sub_123",
                    "email": "new@example.com",
                    "status": "active",
                },
            }
        )

        with (
            patch("familiar.skills.mailerlite.skill.get_env", side_effect=_ml_env),
            patch(HTTPX_POST, return_value=resp),
        ):
            result = add_subscriber({"email": "new@example.com", "name": "New User"})
        assert "subscriber added" in result.lower()
        assert "new@example.com" in result


class TestListCampaigns:
    def test_success(self):
        from familiar.skills.mailerlite.skill import list_campaigns

        resp = _mock_response(
            {
                "data": [
                    {
                        "name": "March Newsletter",
                        "status": "sent",
                        "type": "regular",
                        "created_at": "2026-03-01T10:00:00Z",
                        "stats": {
                            "sent": 500,
                            "opens_count": 200,
                            "clicks_count": 45,
                            "open_rate": {"float": 40.0},
                        },
                    },
                ],
            }
        )

        with (
            patch("familiar.skills.mailerlite.skill.get_env", side_effect=_ml_env),
            patch(HTTPX_GET, return_value=resp),
            patch(CACHE_GET, return_value=None),
            patch(CACHE_SET),
        ):
            result = list_campaigns({})
        assert "March Newsletter" in result
        assert "Sent: 500" in result
        assert "40.0%" in result


class TestCampaignStats:
    def test_no_id(self):
        from familiar.skills.mailerlite.skill import campaign_stats

        with patch("familiar.skills.mailerlite.skill.get_env", side_effect=_ml_env):
            result = campaign_stats({"campaign_id": ""})
        assert "provide a campaign_id" in result.lower()

    def test_success(self):
        from familiar.skills.mailerlite.skill import campaign_stats

        resp = _mock_response(
            {
                "data": {
                    "name": "Year-End Appeal",
                    "stats": {
                        "sent": 1000,
                        "opens_count": 450,
                        "unique_opens_count": 350,
                        "clicks_count": 120,
                        "unique_clicks_count": 80,
                        "unsubscribes_count": 5,
                        "hard_bounces_count": 2,
                        "soft_bounces_count": 3,
                        "open_rate": {"float": 35.0},
                        "click_rate": {"float": 8.0},
                    },
                },
            }
        )

        with (
            patch("familiar.skills.mailerlite.skill.get_env", side_effect=_ml_env),
            patch(HTTPX_GET, return_value=resp),
            patch(CACHE_GET, return_value=None),
            patch(CACHE_SET),
        ):
            result = campaign_stats({"campaign_id": "camp_123"})
        assert "Year-End Appeal" in result
        assert "35.0%" in result
        assert "Bounces: 5" in result


class TestMailerLiteTools:
    def test_tools_registered(self):
        from familiar.skills.mailerlite.skill import TOOLS

        names = [t["name"] for t in TOOLS]
        assert "list_subscribers" in names
        assert "add_subscriber" in names
        assert "list_campaigns" in names
        assert "campaign_stats" in names

    def test_all_have_handlers(self):
        from familiar.skills.mailerlite.skill import TOOLS

        for tool in TOOLS:
            assert callable(tool["handler"])
