# 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 4 skills: Etsy, Cloudinary, Shippo."""

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"

ETSY_ENV = {"ETSY_API_KEY": "key", "ETSY_SHOP_ID": "12345", "ETSY_ACCESS_TOKEN": "token"}
CLOUD_ENV = {
    "CLOUDINARY_CLOUD_NAME": "mycloud",
    "CLOUDINARY_API_KEY": "apikey",
    "CLOUDINARY_API_SECRET": "secret",
}
SHIPPO_ENV = {"SHIPPO_API_KEY": "shippo_key"}


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


def _cloud_env(key):
    return CLOUD_ENV.get(key, "")


def _shippo_env(key):
    return SHIPPO_ENV.get(key, "")


# ── Etsy ─────────────────────────────────────────────────────────────────


class TestEtsyListings:
    def test_no_config(self):
        from familiar.skills.etsy.skill import etsy_listings

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

    def test_success(self):
        from familiar.skills.etsy.skill import etsy_listings

        resp = _mock_response(
            {
                "count": 2,
                "results": [
                    {
                        "listing_id": 111,
                        "title": "Hand-painted Vase",
                        "price": {"amount": 4500, "divisor": 100, "currency_code": "USD"},
                        "quantity": 3,
                        "views": 150,
                        "num_favorers": 12,
                    },
                    {
                        "listing_id": 222,
                        "title": "Ceramic Bowl Set",
                        "price": {"amount": 7500, "divisor": 100, "currency_code": "USD"},
                        "quantity": 1,
                        "views": 80,
                        "num_favorers": 5,
                    },
                ],
            }
        )

        with (
            patch("familiar.skills.etsy.skill.get_env", side_effect=_etsy_env),
            patch(HTTPX_GET, return_value=resp),
            patch(CACHE_GET, return_value=None),
            patch(CACHE_SET),
        ):
            result = etsy_listings({})
        assert "Hand-painted Vase" in result
        assert "$45.00" in result
        assert "150 views" in result


class TestEtsyOrders:
    def test_no_token(self):
        from familiar.skills.etsy.skill import etsy_orders

        env = {"ETSY_API_KEY": "key", "ETSY_SHOP_ID": "123", "ETSY_ACCESS_TOKEN": ""}
        with patch("familiar.skills.etsy.skill.get_env", side_effect=lambda k: env.get(k, "")):
            result = etsy_orders({})
        assert "access_token" in result.lower()

    def test_success(self):
        from familiar.skills.etsy.skill import etsy_orders

        resp = _mock_response(
            {
                "count": 1,
                "results": [
                    {
                        "receipt_id": 9999,
                        "name": "Jane Doe",
                        "grandtotal": {"amount": 4500, "divisor": 100, "currency_code": "USD"},
                        "status": "paid",
                        "transactions": [{"title": "Hand-painted Vase", "quantity": 1}],
                    }
                ],
            }
        )

        with (
            patch("familiar.skills.etsy.skill.get_env", side_effect=_etsy_env),
            patch(HTTPX_GET, return_value=resp),
            patch(CACHE_GET, return_value=None),
            patch(CACHE_SET),
        ):
            result = etsy_orders({})
        assert "Jane Doe" in result
        assert "9999" in result
        assert "$45.00" in result


class TestEtsyStats:
    def test_success(self):
        from familiar.skills.etsy.skill import etsy_stats

        resp = _mock_response(
            {
                "shop_name": "ArtisanStudio",
                "listing_active_count": 15,
                "transaction_sold_count": 234,
                "review_count": 45,
                "review_average": 4.8,
                "num_favorers": 320,
                "currency_code": "USD",
                "url": "https://www.etsy.com/shop/ArtisanStudio",
            }
        )

        with (
            patch("familiar.skills.etsy.skill.get_env", side_effect=_etsy_env),
            patch(HTTPX_GET, return_value=resp),
            patch(CACHE_GET, return_value=None),
            patch(CACHE_SET),
        ):
            result = etsy_stats({})
        assert "ArtisanStudio" in result
        assert "234" in result
        assert "4.8" in result


class TestEtsyCreateListing:
    def test_no_title(self):
        from familiar.skills.etsy.skill import etsy_create_listing

        with patch("familiar.skills.etsy.skill.get_env", side_effect=_etsy_env):
            result = etsy_create_listing({"title": "", "description": "d", "price": 10})
        assert "provide a listing title" in result.lower()

    def test_success(self):
        from familiar.skills.etsy.skill import etsy_create_listing

        resp = _mock_response({"listing_id": 55555})

        with (
            patch("familiar.skills.etsy.skill.get_env", side_effect=_etsy_env),
            patch(HTTPX_POST, return_value=resp),
        ):
            result = etsy_create_listing(
                {"title": "New Art Print", "description": "A beautiful print", "price": 25.00}
            )
        assert "55555" in result
        assert "draft" in result.lower()


class TestEtsyTools:
    def test_tools_registered(self):
        from familiar.skills.etsy.skill import TOOLS

        names = [t["name"] for t in TOOLS]
        assert "etsy_listings" in names
        assert "etsy_orders" in names
        assert "etsy_stats" in names
        assert "etsy_create_listing" in names
        assert "etsy_update_listing" in names

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

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


# ── Cloudinary ───────────────────────────────────────────────────────────


class TestGalleryImages:
    def test_no_config(self):
        from familiar.skills.cloudinary_img.skill import gallery_images

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

    def test_success(self):
        from familiar.skills.cloudinary_img.skill import gallery_images

        resp = _mock_response(
            {
                "total_count": 2,
                "resources": [
                    {
                        "public_id": "art/painting-01",
                        "format": "jpg",
                        "width": 1920,
                        "height": 1080,
                        "bytes": 245760,
                        "created_at": "2026-03-01T10:00:00Z",
                        "secure_url": "https://res.cloudinary.com/mycloud/art/painting-01.jpg",
                    }
                ],
            }
        )

        with (
            patch("familiar.skills.cloudinary_img.skill.get_env", side_effect=_cloud_env),
            patch(HTTPX_GET, return_value=resp),
            patch(CACHE_GET, return_value=None),
            patch(CACHE_SET),
        ):
            result = gallery_images({})
        assert "painting-01" in result
        assert "1920x1080" in result


class TestUploadImage:
    def test_no_url(self):
        from familiar.skills.cloudinary_img.skill import upload_image

        with patch("familiar.skills.cloudinary_img.skill.get_env", side_effect=_cloud_env):
            result = upload_image({"url": ""})
        assert "provide an image url" in result.lower()

    def test_success(self):
        from familiar.skills.cloudinary_img.skill import upload_image

        resp = _mock_response(
            {
                "public_id": "portfolio/new-piece",
                "format": "png",
                "width": 800,
                "height": 600,
                "bytes": 102400,
                "secure_url": "https://res.cloudinary.com/mycloud/portfolio/new-piece.png",
            }
        )

        with (
            patch("familiar.skills.cloudinary_img.skill.get_env", side_effect=_cloud_env),
            patch(HTTPX_POST, return_value=resp),
        ):
            result = upload_image({"url": "https://example.com/image.png", "folder": "portfolio"})
        assert "uploaded successfully" in result.lower()
        assert "new-piece" in result


class TestTransformImage:
    def test_no_transforms(self):
        from familiar.skills.cloudinary_img.skill import transform_image

        with patch("familiar.skills.cloudinary_img.skill.get_env", side_effect=_cloud_env):
            result = transform_image({"public_id": "test"})
        assert "provide at least one transformation" in result.lower()

    def test_generates_url(self):
        from familiar.skills.cloudinary_img.skill import transform_image

        with patch("familiar.skills.cloudinary_img.skill.get_env", side_effect=_cloud_env):
            result = transform_image(
                {"public_id": "art/piece", "width": 300, "height": 300, "crop": "thumb"}
            )
        assert "w_300" in result
        assert "h_300" in result
        assert "c_thumb" in result
        assert "mycloud" in result


class TestImageUrl:
    def test_no_id(self):
        from familiar.skills.cloudinary_img.skill import image_url

        with patch("familiar.skills.cloudinary_img.skill.get_env", side_effect=_cloud_env):
            result = image_url({"public_id": ""})
        assert "provide a public_id" in result.lower()

    def test_generates_urls(self):
        from familiar.skills.cloudinary_img.skill import image_url

        with patch("familiar.skills.cloudinary_img.skill.get_env", side_effect=_cloud_env):
            result = image_url({"public_id": "my-art"})
        assert "Original" in result
        assert "Thumbnail" in result
        assert "mycloud" in result


class TestCloudinaryTools:
    def test_tools_registered(self):
        from familiar.skills.cloudinary_img.skill import TOOLS

        names = [t["name"] for t in TOOLS]
        assert "gallery_images" in names
        assert "upload_image" in names
        assert "transform_image" in names
        assert "image_url" in names


# ── Shippo ───────────────────────────────────────────────────────────────


class TestShippingRates:
    def test_no_config(self):
        from familiar.skills.shippo.skill import shipping_rates

        with patch("familiar.skills.shippo.skill.get_env", return_value=""):
            result = shipping_rates({"from_zip": "97201", "to_zip": "10001", "weight": 2})
        assert "not configured" in result.lower()

    def test_missing_zip(self):
        from familiar.skills.shippo.skill import shipping_rates

        with patch("familiar.skills.shippo.skill.get_env", side_effect=_shippo_env):
            result = shipping_rates({"from_zip": "", "to_zip": "10001", "weight": 2})
        assert "provide from_zip" in result.lower()

    def test_missing_weight(self):
        from familiar.skills.shippo.skill import shipping_rates

        with patch("familiar.skills.shippo.skill.get_env", side_effect=_shippo_env):
            result = shipping_rates({"from_zip": "97201", "to_zip": "10001"})
        assert "provide package weight" in result.lower()

    def test_success(self):
        from familiar.skills.shippo.skill import shipping_rates

        resp = _mock_response(
            {
                "rates": [
                    {
                        "provider": "USPS",
                        "servicelevel": {"name": "Priority Mail"},
                        "amount": "8.50",
                        "currency": "USD",
                        "estimated_days": 3,
                        "object_id": "rate_abc123",
                    },
                    {
                        "provider": "UPS",
                        "servicelevel": {"name": "Ground"},
                        "amount": "12.00",
                        "currency": "USD",
                        "estimated_days": 5,
                        "object_id": "rate_def456",
                    },
                ]
            }
        )

        with (
            patch("familiar.skills.shippo.skill.get_env", side_effect=_shippo_env),
            patch(HTTPX_POST, return_value=resp),
        ):
            result = shipping_rates({"from_zip": "97201", "to_zip": "10001", "weight": 2})
        assert "USPS" in result
        assert "Priority Mail" in result
        assert "$8.50" in result
        assert "rate_abc123" in result


class TestCreateLabel:
    def test_no_rate_id(self):
        from familiar.skills.shippo.skill import create_label

        with patch("familiar.skills.shippo.skill.get_env", side_effect=_shippo_env):
            result = create_label({"rate_id": ""})
        assert "provide a rate_id" in result.lower()

    def test_success(self):
        from familiar.skills.shippo.skill import create_label

        resp = _mock_response(
            {
                "status": "SUCCESS",
                "tracking_number": "9400111899223",
                "label_url": "https://shippo-delivery.s3.amazonaws.com/label.pdf",
                "object_id": "txn_789",
            }
        )

        with (
            patch("familiar.skills.shippo.skill.get_env", side_effect=_shippo_env),
            patch(HTTPX_POST, return_value=resp),
        ):
            result = create_label({"rate_id": "rate_abc123"})
        assert "9400111899223" in result
        assert "label.pdf" in result


class TestTrackShipment:
    def test_no_tracking(self):
        from familiar.skills.shippo.skill import track_shipment

        with patch("familiar.skills.shippo.skill.get_env", side_effect=_shippo_env):
            result = track_shipment({"carrier": "usps", "tracking_number": ""})
        assert "provide a tracking_number" in result.lower()

    def test_no_carrier(self):
        from familiar.skills.shippo.skill import track_shipment

        with patch("familiar.skills.shippo.skill.get_env", side_effect=_shippo_env):
            result = track_shipment({"carrier": "", "tracking_number": "123"})
        assert "provide the carrier" in result.lower()

    def test_success(self):
        from familiar.skills.shippo.skill import track_shipment

        resp = _mock_response(
            {
                "carrier": "USPS",
                "tracking_status": {
                    "status": "IN_TRANSIT",
                    "status_details": "In transit to destination",
                    "location": {"city": "Portland", "state": "OR"},
                },
                "eta": "2026-03-08",
                "tracking_history": [
                    {
                        "status": "TRANSIT",
                        "status_details": "Departed facility",
                        "status_date": "2026-03-06T10:00:00Z",
                        "location": {"city": "Portland", "state": "OR"},
                    }
                ],
            }
        )

        with (
            patch("familiar.skills.shippo.skill.get_env", side_effect=_shippo_env),
            patch(HTTPX_GET, return_value=resp),
            patch(CACHE_GET, return_value=None),
            patch(CACHE_SET),
        ):
            result = track_shipment({"carrier": "usps", "tracking_number": "9400111899223"})
        assert "IN_TRANSIT" in result
        assert "Portland" in result


class TestShippoTools:
    def test_tools_registered(self):
        from familiar.skills.shippo.skill import TOOLS

        names = [t["name"] for t in TOOLS]
        assert "shipping_rates" in names
        assert "create_label" in names
        assert "track_shipment" in names

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

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