# 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 7 skills: HubSpot, Todoist, Clockify, QuickBooks."""

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"

HUBSPOT_ENV = {"HUBSPOT_ACCESS_TOKEN": "hs_tok"}
TODOIST_ENV = {"TODOIST_API_TOKEN": "td_tok"}
CLOCKIFY_ENV = {"CLOCKIFY_API_KEY": "ck_key", "CLOCKIFY_WORKSPACE_ID": "ws_123"}
QB_ENV = {"QUICKBOOKS_ACCESS_TOKEN": "qb_tok", "QUICKBOOKS_REALM_ID": "realm_123"}


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 _hs_env(key):
    return HUBSPOT_ENV.get(key, "")


def _td_env(key):
    return TODOIST_ENV.get(key, "")


def _ck_env(key):
    return CLOCKIFY_ENV.get(key, "")


def _qb_env(key):
    return QB_ENV.get(key, "")


# ── HubSpot ─────────────────────────────────────────────────────────────


class TestHubSpotContacts:
    def test_no_config(self):
        from familiar.skills.hubspot.skill import hubspot_contacts

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

    def test_success(self):
        from familiar.skills.hubspot.skill import hubspot_contacts

        resp = _mock_response(
            {
                "results": [
                    {
                        "id": "101",
                        "properties": {
                            "firstname": "Jane",
                            "lastname": "Doe",
                            "email": "jane@example.com",
                            "company": "Acme Inc",
                            "phone": "555-1234",
                        },
                    },
                ],
            }
        )

        with (
            patch("familiar.skills.hubspot.skill.get_env", side_effect=_hs_env),
            patch(HTTPX_GET, return_value=resp),
            patch(CACHE_GET, return_value=None),
            patch(CACHE_SET),
        ):
            result = hubspot_contacts({})
        assert "Jane Doe" in result
        assert "jane@example.com" in result
        assert "Acme Inc" in result


class TestHubSpotDeals:
    def test_success(self):
        from familiar.skills.hubspot.skill import hubspot_deals

        resp = _mock_response(
            {
                "results": [
                    {
                        "id": "201",
                        "properties": {
                            "dealname": "Enterprise Contract",
                            "dealstage": "closedwon",
                            "amount": "50000",
                            "closedate": "2026-03-15T00:00:00Z",
                        },
                    },
                ],
            }
        )

        with (
            patch("familiar.skills.hubspot.skill.get_env", side_effect=_hs_env),
            patch(HTTPX_GET, return_value=resp),
            patch(CACHE_GET, return_value=None),
            patch(CACHE_SET),
        ):
            result = hubspot_deals({})
        assert "Enterprise Contract" in result
        assert "$50,000.00" in result


class TestHubSpotAddContact:
    def test_no_email(self):
        from familiar.skills.hubspot.skill import hubspot_add_contact

        with patch("familiar.skills.hubspot.skill.get_env", side_effect=_hs_env):
            result = hubspot_add_contact({"email": ""})
        assert "provide an email" in result.lower()

    def test_success(self):
        from familiar.skills.hubspot.skill import hubspot_add_contact

        resp = _mock_response({"id": "301"})

        with (
            patch("familiar.skills.hubspot.skill.get_env", side_effect=_hs_env),
            patch(HTTPX_POST, return_value=resp),
        ):
            result = hubspot_add_contact(
                {"email": "new@example.com", "firstname": "New", "lastname": "User"}
            )
        assert "contact created" in result.lower()
        assert "301" in result


class TestHubSpotSearch:
    def test_no_query(self):
        from familiar.skills.hubspot.skill import hubspot_search

        with patch("familiar.skills.hubspot.skill.get_env", side_effect=_hs_env):
            result = hubspot_search({"query": ""})
        assert "provide a search query" in result.lower()

    def test_success(self):
        from familiar.skills.hubspot.skill import hubspot_search

        resp = _mock_response(
            {
                "total": 1,
                "results": [
                    {
                        "id": "101",
                        "properties": {
                            "firstname": "Jane",
                            "lastname": "Doe",
                            "email": "jane@example.com",
                        },
                    },
                ],
            }
        )

        with (
            patch("familiar.skills.hubspot.skill.get_env", side_effect=_hs_env),
            patch(HTTPX_POST, return_value=resp),
        ):
            result = hubspot_search({"query": "Jane"})
        assert "Jane Doe" in result


class TestHubSpotTools:
    def test_tools_registered(self):
        from familiar.skills.hubspot.skill import TOOLS

        names = [t["name"] for t in TOOLS]
        assert "hubspot_contacts" in names
        assert "hubspot_deals" in names
        assert "hubspot_add_contact" in names
        assert "hubspot_search" in names


# ── Todoist ──────────────────────────────────────────────────────────────


class TestTodoistTasks:
    def test_no_config(self):
        from familiar.skills.todoist.skill import todoist_tasks

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

    def test_success(self):
        from familiar.skills.todoist.skill import todoist_tasks

        resp = _mock_response(
            [
                {
                    "id": "t001",
                    "content": "Review Q1 report",
                    "priority": 4,
                    "due": {"date": "2026-03-10"},
                    "project_id": "p001",
                    "labels": ["work"],
                },
            ]
        )

        with (
            patch("familiar.skills.todoist.skill.get_env", side_effect=_td_env),
            patch(HTTPX_GET, return_value=resp),
            patch(CACHE_GET, return_value=None),
            patch(CACHE_SET),
        ):
            result = todoist_tasks({})
        assert "Review Q1 report" in result
        assert "P1" in result
        assert "2026-03-10" in result


class TestTodoistAddTask:
    def test_no_content(self):
        from familiar.skills.todoist.skill import todoist_add_task

        with patch("familiar.skills.todoist.skill.get_env", side_effect=_td_env):
            result = todoist_add_task({"content": ""})
        assert "provide task content" in result.lower()

    def test_success(self):
        from familiar.skills.todoist.skill import todoist_add_task

        resp = _mock_response({"id": "t002", "url": "https://todoist.com/task/t002"})

        with (
            patch("familiar.skills.todoist.skill.get_env", side_effect=_td_env),
            patch(HTTPX_POST, return_value=resp),
        ):
            result = todoist_add_task({"content": "Call dentist", "due_string": "tomorrow"})
        assert "task created" in result.lower()
        assert "t002" in result


class TestTodoistComplete:
    def test_no_id(self):
        from familiar.skills.todoist.skill import todoist_complete

        with patch("familiar.skills.todoist.skill.get_env", side_effect=_td_env):
            result = todoist_complete({"task_id": ""})
        assert "provide a task_id" in result.lower()

    def test_success(self):
        from familiar.skills.todoist.skill import todoist_complete

        resp = _mock_response({})

        with (
            patch("familiar.skills.todoist.skill.get_env", side_effect=_td_env),
            patch(HTTPX_POST, return_value=resp),
        ):
            result = todoist_complete({"task_id": "t001"})
        assert "completed" in result.lower()


class TestTodoistProjects:
    def test_success(self):
        from familiar.skills.todoist.skill import todoist_projects

        resp = _mock_response(
            [
                {"id": "p001", "name": "Work", "color": "blue", "is_favorite": True},
                {"id": "p002", "name": "Personal", "color": "green", "is_favorite": False},
            ]
        )

        with (
            patch("familiar.skills.todoist.skill.get_env", side_effect=_td_env),
            patch(HTTPX_GET, return_value=resp),
            patch(CACHE_GET, return_value=None),
            patch(CACHE_SET),
        ):
            result = todoist_projects({})
        assert "Work" in result
        assert "Personal" in result


class TestTodoistTools:
    def test_tools_registered(self):
        from familiar.skills.todoist.skill import TOOLS

        names = [t["name"] for t in TOOLS]
        assert "todoist_tasks" in names
        assert "todoist_add_task" in names
        assert "todoist_complete" in names
        assert "todoist_projects" in names


# ── Clockify ─────────────────────────────────────────────────────────────


class TestStartTimer:
    def test_no_config(self):
        from familiar.skills.clockify.skill import start_timer

        with patch("familiar.skills.clockify.skill.get_env", return_value=""):
            result = start_timer({"description": "Work"})
        assert "not configured" in result.lower()

    def test_no_description(self):
        from familiar.skills.clockify.skill import start_timer

        with patch("familiar.skills.clockify.skill.get_env", side_effect=_ck_env):
            result = start_timer({"description": ""})
        assert "provide a description" in result.lower()

    def test_success(self):
        from familiar.skills.clockify.skill import start_timer

        resp = _mock_response({"id": "entry_001"})

        with (
            patch("familiar.skills.clockify.skill.get_env", side_effect=_ck_env),
            patch(HTTPX_POST, return_value=resp),
        ):
            result = start_timer({"description": "Client meeting"})
        assert "timer started" in result.lower()
        assert "entry_001" in result


class TestStopTimer:
    def test_success(self):
        from familiar.skills.clockify.skill import stop_timer

        user_resp = _mock_response({"id": "user_001"})
        stop_resp = _mock_response(
            {
                "description": "Client meeting",
                "timeInterval": {"duration": "PT1H30M"},
            }
        )

        def _get_side(*args, **kwargs):
            return user_resp

        def _post_side(*args, **kwargs):
            return stop_resp

        with (
            patch("familiar.skills.clockify.skill.get_env", side_effect=_ck_env),
            patch(HTTPX_GET, side_effect=_get_side),
            patch(HTTPX_POST, side_effect=_post_side),
        ):
            result = stop_timer({})
        assert "timer stopped" in result.lower()
        assert "PT1H30M" in result


class TestLogTime:
    def test_no_description(self):
        from familiar.skills.clockify.skill import log_time

        with patch("familiar.skills.clockify.skill.get_env", side_effect=_ck_env):
            result = log_time({"description": "", "start": "a", "end": "b"})
        assert "provide a description" in result.lower()

    def test_success(self):
        from familiar.skills.clockify.skill import log_time

        resp = _mock_response({"id": "entry_002", "timeInterval": {"duration": "PT2H"}})

        with (
            patch("familiar.skills.clockify.skill.get_env", side_effect=_ck_env),
            patch(HTTPX_POST, return_value=resp),
        ):
            result = log_time(
                {
                    "description": "Design work",
                    "start": "2026-03-06T09:00:00Z",
                    "end": "2026-03-06T11:00:00Z",
                }
            )
        assert "time logged" in result.lower()
        assert "PT2H" in result


class TestTimeReport:
    def test_success(self):
        from familiar.skills.clockify.skill import time_report

        user_resp = _mock_response({"id": "user_001"})
        entries_resp = _mock_response(
            [
                {
                    "description": "Code review",
                    "timeInterval": {"start": "2026-03-06T09:00:00Z", "duration": "PT45M"},
                    "project": {"name": "Familiar"},
                },
            ]
        )

        with (
            patch("familiar.skills.clockify.skill.get_env", side_effect=_ck_env),
            patch(
                HTTPX_GET,
                side_effect=lambda *a, **kw: entries_resp if "/time-entries" in str(a) else user_resp,
            ),
            patch(CACHE_GET, return_value=None),
            patch(CACHE_SET),
        ):
            result = time_report({})
        assert "Code review" in result


class TestClockifyProjects:
    def test_success(self):
        from familiar.skills.clockify.skill import clockify_projects

        resp = _mock_response(
            [
                {"id": "proj_001", "name": "Familiar", "color": "#00f", "clientName": "Internal"},
            ]
        )

        with (
            patch("familiar.skills.clockify.skill.get_env", side_effect=_ck_env),
            patch(HTTPX_GET, return_value=resp),
            patch(CACHE_GET, return_value=None),
            patch(CACHE_SET),
        ):
            result = clockify_projects({})
        assert "Familiar" in result
        assert "Internal" in result


class TestClockifyTools:
    def test_tools_registered(self):
        from familiar.skills.clockify.skill import TOOLS

        names = [t["name"] for t in TOOLS]
        assert "start_timer" in names
        assert "stop_timer" in names
        assert "log_time" in names
        assert "time_report" in names
        assert "clockify_projects" in names


# ── QuickBooks ───────────────────────────────────────────────────────────


class TestQbInvoices:
    def test_no_config(self):
        from familiar.skills.quickbooks.skill import qb_invoices

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

    def test_success(self):
        from familiar.skills.quickbooks.skill import qb_invoices

        resp = _mock_response(
            {
                "QueryResponse": {
                    "Invoice": [
                        {
                            "DocNumber": "1001",
                            "TotalAmt": 2500.00,
                            "Balance": 0,
                            "DueDate": "2026-04-01",
                            "CustomerRef": {"name": "Acme Corp"},
                            "CurrencyRef": {"value": "USD"},
                        },
                    ],
                }
            }
        )

        with (
            patch("familiar.skills.quickbooks.skill.get_env", side_effect=_qb_env),
            patch(HTTPX_GET, return_value=resp),
            patch(CACHE_GET, return_value=None),
            patch(CACHE_SET),
        ):
            result = qb_invoices({})
        assert "1001" in result
        assert "$2500.00" in result
        assert "Paid" in result


class TestQbExpenses:
    def test_success(self):
        from familiar.skills.quickbooks.skill import qb_expenses

        resp = _mock_response(
            {
                "QueryResponse": {
                    "Purchase": [
                        {
                            "TotalAmt": 150.00,
                            "TxnDate": "2026-03-01",
                            "PaymentType": "CreditCard",
                            "EntityRef": {"name": "Office Depot"},
                            "CurrencyRef": {"value": "USD"},
                        },
                    ],
                }
            }
        )

        with (
            patch("familiar.skills.quickbooks.skill.get_env", side_effect=_qb_env),
            patch(HTTPX_GET, return_value=resp),
            patch(CACHE_GET, return_value=None),
            patch(CACHE_SET),
        ):
            result = qb_expenses({})
        assert "$150.00" in result
        assert "Office Depot" in result


class TestQbProfitLoss:
    def test_success(self):
        from familiar.skills.quickbooks.skill import qb_profit_loss

        resp = _mock_response(
            {
                "Header": {
                    "ReportName": "Profit and Loss",
                    "StartPeriod": "2026-01-01",
                    "EndPeriod": "2026-03-31",
                },
                "Rows": {
                    "Row": [
                        {
                            "type": "Section",
                            "Header": {"ColData": [{"value": "Income"}]},
                            "Rows": {
                                "Row": [
                                    {"ColData": [{"value": "Sales"}, {"value": "10000"}]},
                                ]
                            },
                            "Summary": {"ColData": [{"value": "Total Income"}, {"value": "10000"}]},
                        },
                    ]
                },
            }
        )

        with (
            patch("familiar.skills.quickbooks.skill.get_env", side_effect=_qb_env),
            patch(HTTPX_GET, return_value=resp),
            patch(CACHE_GET, return_value=None),
            patch(CACHE_SET),
        ):
            result = qb_profit_loss({})
        assert "Profit and Loss" in result
        assert "$10,000.00" in result


class TestQbCustomers:
    def test_success(self):
        from familiar.skills.quickbooks.skill import qb_customers

        resp = _mock_response(
            {
                "QueryResponse": {
                    "Customer": [
                        {
                            "Id": "42",
                            "DisplayName": "Big Client LLC",
                            "PrimaryEmailAddr": {"Address": "contact@bigclient.com"},
                            "PrimaryPhone": {"FreeFormNumber": "555-9876"},
                            "Balance": 1200.00,
                        },
                    ],
                }
            }
        )

        with (
            patch("familiar.skills.quickbooks.skill.get_env", side_effect=_qb_env),
            patch(HTTPX_GET, return_value=resp),
            patch(CACHE_GET, return_value=None),
            patch(CACHE_SET),
        ):
            result = qb_customers({})
        assert "Big Client LLC" in result
        assert "contact@bigclient.com" in result
        assert "$1200.00" in result


class TestQuickBooksTools:
    def test_tools_registered(self):
        from familiar.skills.quickbooks.skill import TOOLS

        names = [t["name"] for t in TOOLS]
        assert "qb_invoices" in names
        assert "qb_expenses" in names
        assert "qb_profit_loss" in names
        assert "qb_customers" in names
