#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Tests for Dify in-memory integration of notify_to_cisco_webex.Webex.

These tests verify that:
- Dify-provided file references are downloaded into memory and uploaded to Webex
  without writing temporary files to disk.
- Relative URLs require a base URL to be provided.
- The multipart uploads include the correct filename and mime detection.
"""

from __future__ import annotations

import io
import os
from typing import Any, Dict, List, Optional

import pytest

from notify_to_cisco_webex.notify_to_cisco_webex import Webex, WebexError


class FakeGetResponse:
    def __init__(self, content: bytes, content_type: Optional[str] = None) -> None:
        self.content = content
        self.status_code = 200
        self._headers = {}
        if content_type:
            self._headers["Content-Type"] = content_type

    @property
    def headers(self) -> Dict[str, str]:
        return self._headers

    def raise_for_status(self) -> None:
        # Always OK in these tests for success paths
        return None


class FakePostResponse:
    def __init__(self, payload: Dict[str, Any]) -> None:
        self.status_code = 200
        self._payload = payload

    def json(self) -> Dict[str, Any]:
        return self._payload

    @property
    def text(self) -> str:
        import json

        try:
            return json.dumps(self._payload)
        except Exception:
            return str(self._payload)


class FakeClient:
    """
    Fake httpx.Client replacement that supports both `get` (for Dify downloads)
    and `post` (for Webex uploads). It records calls to class-level lists so
    tests can inspect them.
    """

    # Test setups should set these mappings/lists as needed
    url_map: Dict[str, bytes] = {}
    get_calls: List[Dict[str, Any]] = []
    post_calls: List[Dict[str, Any]] = []

    def __init__(self, *args, **kwargs) -> None:
        # Ensure we have fresh call logs for each test run (constructor may be called multiple times)
        # Tests will reset these explicitly, but keep safe behavior here.
        # Do not clear in every __init__ to allow separate instances used across contexts.
        pass

    def __enter__(self) -> "FakeClient":
        return self

    def __exit__(self, exc_type, exc, tb) -> None:
        return None

    def get(self, url: str, headers: Optional[Dict[str, str]] = None, **kwargs):
        # Record the download attempt
        self.__class__.get_calls.append({"url": url, "headers": headers})
        if url not in self.__class__.url_map:
            # Simulate a 404-like behavior by raising an httpx-like error wrapped as our tests expect
            raise RuntimeError(f"URL not found in fake map: {url}")
        content = self.__class__.url_map[url]
        # Provide a content-type hint if available by inspecting filename
        ctype = None
        if url.lower().endswith(".png"):
            ctype = "image/png"
        elif url.lower().endswith(".jpg") or url.lower().endswith(".jpeg"):
            ctype = "image/jpeg"
        elif url.lower().endswith(".pdf"):
            ctype = "application/pdf"
        return FakeGetResponse(content, content_type=ctype)

    def post(
        self,
        url: str,
        data: Optional[Dict[str, Any]] = None,
        files: Optional[Dict[str, Any]] = None,
        json: Optional[Dict[str, Any]] = None,
        headers: Optional[Dict[str, str]] = None,
        **kwargs,
    ):
        # Record post parameters for assertions
        record = {
            "url": url,
            "data": data,
            "files": files,
            "json": json,
            "headers": headers,
        }
        self.__class__.post_calls.append(record)
        # Return a dummy success response that resembles Webex API
        return FakePostResponse({"id": "fake-msg-id", "ok": True})


def test_send_message_from_dify_files_in_memory_absolute_urls(monkeypatch, tmp_path):
    """
    Verifies that Dify file URLs which are absolute are fetched and uploaded
    entirely in-memory, and that no temporary files are created on disk.
    """
    # Prepare fake binary contents for two files
    url_a = "https://dify.example/files/eed36c5d-nyansu.png"
    url_b = "https://dify.example/files/889b3b51-banao.jpg"
    FakeClient.url_map = {
        url_a: b"\x89PNG\r\n\x1a\nPNG-DATA",
        url_b: b"\xff\xd8\xffJPEG-DATA",
    }
    FakeClient.get_calls = []
    FakeClient.post_calls = []

    # Patch httpx.Client used by our module to FakeClient
    monkeypatch.setattr(
        "notify_to_cisco_webex.notify_to_cisco_webex.httpx.Client",
        FakeClient,
    )

    # Prevent any accidental disk tempfile creation by making NamedTemporaryFile raise
    import tempfile

    def _fail_tmp(*args, **kwargs):
        raise AssertionError(
            "No temporary files should be created when processing Dify files in-memory"
        )

    monkeypatch.setattr(tempfile, "NamedTemporaryFile", _fail_tmp)

    client = Webex(access_token="token", room_id="room-id")

    # Simulate Dify-provided file structures (dicts or objects are supported)
    dify_files = [
        {"url": url_a, "filename": "nyansu.png"},
        {"url": url_b, "filename": "banao.jpg"},
    ]

    result = client.send_message_from_dify_files(
        message="Here are your images", format="markdown", dify_files=dify_files
    )

    # Should have returned a list of responses (two uploads)
    assert isinstance(result, list)
    assert len(result) == 2
    assert all(isinstance(r, dict) for r in result)
    assert result[0].get("id") == "fake-msg-id"

    # Verify that downloads occurred for each URL and posts were made for each file
    assert len(FakeClient.get_calls) == 2
    assert {call["url"] for call in FakeClient.get_calls} == {url_a, url_b}

    assert len(FakeClient.post_calls) == 2
    # Inspect the recorded post for the first file
    first_post = FakeClient.post_calls[0]
    assert first_post["url"].endswith("/messages")
    # For file uploads we expect 'data' (form fields) and 'files'
    assert first_post["data"] is not None
    # The markdown message should appear in the first post's data
    assert first_post["data"].get("markdown") == "Here are your images"
    # Files param should include the filename as first element of the tuple
    files_param = first_post["files"]
    assert files_param is not None
    # files_param is expected to be a dict with key 'files' mapping to a tuple
    assert "files" in files_param
    uploaded_filename = files_param["files"][0]
    assert (
        uploaded_filename in ("nyansu.png", "banao.jpg")
        or uploaded_filename == "nyansu.png"
    )

    # Ensure no temporary files exist in tmp_path (defensive)
    tmp_files = list(tmp_path.iterdir())
    assert tmp_files == [] or all(not p.name.startswith("tmp") for p in tmp_files)


def test_send_message_from_dify_files_relative_urls_require_base(monkeypatch):
    """
    Verifies that relative Dify URLs raise an error if dify_base_url is not provided,
    and succeed when dify_base_url is given.
    """
    FakeClient.url_map = {}
    FakeClient.get_calls = []
    FakeClient.post_calls = []

    monkeypatch.setattr(
        "notify_to_cisco_webex.notify_to_cisco_webex.httpx.Client",
        FakeClient,
    )

    client = Webex(access_token="token", room_id="room-id")

    # Relative URL without base should raise
    dify_files_rel = [{"url": "/files/relative/one.png", "filename": "one.png"}]
    with pytest.raises(WebexError):
        client.send_message_from_dify_files(
            message="x", format="markdown", dify_files=dify_files_rel
        )

    # Now provide base and map the resolved absolute URL in FakeClient
    base = "https://dify.internal"
    resolved = "https://dify.internal/files/relative/one.png"
    FakeClient.url_map = {resolved: b"PNG_DATA"}
    FakeClient.get_calls = []
    FakeClient.post_calls = []

    result = client.send_message_from_dify_files(
        message="ok", format="markdown", dify_files=dify_files_rel, dify_base_url=base
    )

    assert isinstance(result, dict) or isinstance(result, list)
    # One download and one post should have occurred
    assert len(FakeClient.get_calls) == 1
    assert FakeClient.get_calls[0]["url"] == resolved
    assert len(FakeClient.post_calls) == 1
    # Posted filename should be detected as 'one.png'
    posted = FakeClient.post_calls[0]
    files_param = posted["files"]
    assert files_param is not None
    assert "files" in files_param
    assert files_param["files"][0] == "one.png"
