"""Tests for mcp-image-gen server — all ComfyUI HTTP calls mocked via respx."""

import base64
import json
import os
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch

import httpx
import pytest
import respx

# Import the server module (sys.path set by conftest.py)
import server
from server import (
    ComfyUIClient,
    build_flux_workflow,
    generate_image,
    get_generation_status,
    get_output_directory,
    list_available_models,
)

COMFYUI_BASE = "http://test-comfyui:8188"


# ---------------------------------------------------------------------------
# build_flux_workflow — pure function, no mocking needed
# ---------------------------------------------------------------------------

def test_build_flux_workflow_structure():
    """Verify build_flux_workflow returns a dict with correct node types."""
    wf = build_flux_workflow(
        prompt="a red cat",
        neg_prompt="ugly",
        width=512,
        height=768,
        steps=8,
        seed=42,
        model="flux1-schnell.safetensors",
    )
    assert wf["6"]["class_type"] == "CLIPTextEncode"
    assert wf["8"]["class_type"] == "VAEDecode"
    assert wf["9"]["class_type"] == "SaveImage"
    assert wf["13"]["class_type"] == "KSampler"
    assert wf["27"]["class_type"] == "EmptySD3LatentImage"
    assert wf["30"]["class_type"] == "DualCLIPLoader"
    assert wf["31"]["class_type"] == "VAELoader"
    assert wf["32"]["class_type"] == "UNETLoader"
    assert wf["33"]["class_type"] == "CLIPTextEncode"


def test_build_flux_workflow_params_injected():
    """Verify all parameters are injected into correct nodes."""
    wf = build_flux_workflow(
        prompt="a blue whale",
        neg_prompt="cartoonish",
        width=512,
        height=768,
        steps=8,
        seed=12345,
        model="sdxl.safetensors",
    )
    assert wf["6"]["inputs"]["text"] == "a blue whale"
    assert wf["33"]["inputs"]["text"] == "cartoonish"
    assert wf["27"]["inputs"]["width"] == 512
    assert wf["27"]["inputs"]["height"] == 768
    assert wf["13"]["inputs"]["steps"] == 8
    assert wf["13"]["inputs"]["seed"] == 12345
    assert wf["32"]["inputs"]["unet_name"] == "sdxl.safetensors"


def test_negative_prompt_included():
    """Verify negative prompt appears in workflow node 33 when provided."""
    wf = build_flux_workflow(
        prompt="forest",
        neg_prompt="blurry, dark",
        width=1024,
        height=1024,
        steps=4,
        seed=1,
        model="flux1-schnell.safetensors",
    )
    assert wf["33"]["inputs"]["text"] == "blurry, dark"


def test_random_seed_generated():
    """seed=-1 generates a random seed each call."""
    wf1 = build_flux_workflow("cat", "", 512, 512, 4, -1, "flux1-schnell.safetensors")
    wf2 = build_flux_workflow("cat", "", 512, 512, 4, -1, "flux1-schnell.safetensors")
    seed1 = wf1["_meta"]["actual_seed"]
    seed2 = wf2["_meta"]["actual_seed"]
    # Both are valid integers
    assert isinstance(seed1, int)
    assert 0 <= seed1 < 2**32
    # With overwhelming probability they differ
    # (1/2^32 chance of collision — negligible for a test)
    # We just verify _meta is populated
    assert "_meta" in wf1
    assert "_meta" in wf2


# ---------------------------------------------------------------------------
# list_available_models
# ---------------------------------------------------------------------------

@respx.mock
@pytest.mark.asyncio
async def test_list_available_models():
    """Mock /object_info, verify model list is returned."""
    mock_response = {
        "CheckpointLoaderSimple": {
            "input": {
                "required": {
                    "ckpt_name": [
                        ["flux1-schnell.safetensors", "sdxl.safetensors"],
                        {},
                    ]
                }
            }
        }
    }
    respx.get(f"{COMFYUI_BASE}/object_info/CheckpointLoaderSimple").mock(
        return_value=httpx.Response(200, json=mock_response)
    )

    result = await list_available_models()
    assert "flux1-schnell.safetensors" in result
    assert "sdxl.safetensors" in result


@respx.mock
@pytest.mark.asyncio
async def test_list_available_models_comfyui_offline():
    """When ComfyUI is unreachable, list_available_models returns error message."""
    respx.get(f"{COMFYUI_BASE}/object_info/CheckpointLoaderSimple").mock(
        side_effect=httpx.ConnectError("connection refused")
    )

    result = await list_available_models()
    assert len(result) == 1
    assert "not reachable" in result[0].lower()


# ---------------------------------------------------------------------------
# get_generation_status
# ---------------------------------------------------------------------------

@respx.mock
@pytest.mark.asyncio
async def test_get_generation_status_pending(queue_with_pending):
    """prompt_id in queue_pending → status is 'pending'."""
    respx.get(f"{COMFYUI_BASE}/api/queue").mock(
        return_value=httpx.Response(200, json=queue_with_pending)
    )

    result = await get_generation_status("test-uuid-1234")
    assert result["status"] == "pending"
    assert result["prompt_id"] == "test-uuid-1234"


@respx.mock
@pytest.mark.asyncio
async def test_get_generation_status_running(queue_with_running):
    """prompt_id in queue_running → status is 'running'."""
    respx.get(f"{COMFYUI_BASE}/api/queue").mock(
        return_value=httpx.Response(200, json=queue_with_running)
    )

    result = await get_generation_status("test-uuid-1234")
    assert result["status"] == "running"


@respx.mock
@pytest.mark.asyncio
async def test_get_generation_status_complete(queue_empty, mock_history_response):
    """prompt_id not in queue + found in history → status is 'completed'."""
    respx.get(f"{COMFYUI_BASE}/api/queue").mock(
        return_value=httpx.Response(200, json=queue_empty)
    )
    respx.get(f"{COMFYUI_BASE}/api/history/test-uuid-1234").mock(
        return_value=httpx.Response(200, json=mock_history_response)
    )

    result = await get_generation_status("test-uuid-1234")
    assert result["status"] == "completed"


# ---------------------------------------------------------------------------
# get_output_directory
# ---------------------------------------------------------------------------

def test_get_output_directory_default(monkeypatch):
    """No IMAGE_OUTPUT_DIR env var → returns expanded ~/Pictures/mcp-generated."""
    monkeypatch.delenv("IMAGE_OUTPUT_DIR", raising=False)
    monkeypatch.setattr(server, "IMAGE_OUTPUT_DIR", "~/Pictures/mcp-generated")

    result = get_output_directory()
    assert result == str(Path("~/Pictures/mcp-generated").expanduser().resolve())
    assert "~" not in result  # expanded


def test_get_output_directory_custom(monkeypatch, tmp_path):
    """IMAGE_OUTPUT_DIR set → returns that path."""
    custom = str(tmp_path / "custom-output")
    monkeypatch.setenv("IMAGE_OUTPUT_DIR", custom)
    monkeypatch.setattr(server, "IMAGE_OUTPUT_DIR", custom)

    result = get_output_directory()
    assert result == str(Path(custom).expanduser().resolve())


# ---------------------------------------------------------------------------
# generate_image
# ---------------------------------------------------------------------------

@respx.mock
@pytest.mark.asyncio
async def test_generate_image_success(
    tmp_path, sample_image_bytes, mock_history_response, queue_empty, monkeypatch
):
    """Mock full lifecycle: queue → poll done → history → view. Verify outputs."""
    monkeypatch.setattr(server, "IMAGE_OUTPUT_DIR", str(tmp_path))

    # 1. POST /api/prompt → prompt_id
    respx.post(f"{COMFYUI_BASE}/api/prompt").mock(
        return_value=httpx.Response(200, json={"prompt_id": "test-uuid-1234"})
    )
    # 2. GET /api/queue → empty (job done immediately)
    respx.get(f"{COMFYUI_BASE}/api/queue").mock(
        return_value=httpx.Response(200, json=queue_empty)
    )
    # 3. GET /api/history/test-uuid-1234
    respx.get(f"{COMFYUI_BASE}/api/history/test-uuid-1234").mock(
        return_value=httpx.Response(200, json=mock_history_response)
    )
    # 4. GET /api/view → image bytes
    respx.get(f"{COMFYUI_BASE}/api/view").mock(
        return_value=httpx.Response(200, content=sample_image_bytes)
    )

    result = await generate_image(
        prompt="a red cat",
        output_dir=str(tmp_path),
    )

    # Should return [TextContent, ImageContent]
    assert len(result) == 2
    text_content = result[0]
    image_content = result[1]

    # TextContent has path info
    assert "Generated:" in text_content.text
    assert str(tmp_path) in text_content.text

    # ImageContent has valid base64 PNG
    assert image_content.type == "image"
    assert image_content.mimeType == "image/png"
    decoded = base64.b64decode(image_content.data)
    assert decoded[:8] == b"\x89PNG\r\n\x1a\n"  # PNG magic bytes

    # File was actually saved
    saved_files = list(tmp_path.glob("*.png"))
    assert len(saved_files) == 1


@respx.mock
@pytest.mark.asyncio
async def test_generate_image_comfyui_unavailable():
    """ComfyUI unreachable → returns graceful error message as single TextContent."""
    respx.post(f"{COMFYUI_BASE}/api/prompt").mock(
        side_effect=httpx.ConnectError("connection refused")
    )

    result = await generate_image(prompt="a cat")

    assert len(result) == 1
    assert "not reachable" in result[0].text.lower()


@respx.mock
@pytest.mark.asyncio
async def test_generate_image_timeout(monkeypatch, queue_with_pending):
    """Poll loop never completes within timeout → returns timeout error."""
    monkeypatch.setattr(server, "COMFYUI_TIMEOUT", 0)  # instant timeout

    respx.post(f"{COMFYUI_BASE}/api/prompt").mock(
        return_value=httpx.Response(200, json={"prompt_id": "test-uuid-1234"})
    )
    # Queue always shows job pending → never finishes
    respx.get(f"{COMFYUI_BASE}/api/queue").mock(
        return_value=httpx.Response(200, json=queue_with_pending)
    )

    result = await generate_image(prompt="slow image")

    assert len(result) == 1
    assert "timed out" in result[0].text.lower()
    assert "test-uuid-1234" in result[0].text


@respx.mock
@pytest.mark.asyncio
async def test_generate_image_empty_prompt(tmp_path, sample_image_bytes, mock_history_response, queue_empty, monkeypatch):
    """Empty prompt → workflow has empty text in positive node, but generation succeeds."""
    monkeypatch.setattr(server, "IMAGE_OUTPUT_DIR", str(tmp_path))

    respx.post(f"{COMFYUI_BASE}/api/prompt").mock(
        return_value=httpx.Response(200, json={"prompt_id": "test-empty-uuid"})
    )
    respx.get(f"{COMFYUI_BASE}/api/queue").mock(
        return_value=httpx.Response(200, json=queue_empty)
    )
    mock_history_empty = {
        "test-empty-uuid": {
            "outputs": {
                "9": {
                    "images": [
                        {
                            "filename": "mcp-image-gen_00001_.png",
                            "subfolder": "",
                            "type": "output",
                        }
                    ]
                }
            },
            "status": {"completed": True},
        }
    }
    respx.get(f"{COMFYUI_BASE}/api/history/test-empty-uuid").mock(
        return_value=httpx.Response(200, json=mock_history_empty)
    )
    respx.get(f"{COMFYUI_BASE}/api/view").mock(
        return_value=httpx.Response(200, content=sample_image_bytes)
    )

    result = await generate_image(prompt="", output_dir=str(tmp_path))

    assert len(result) == 2
    text_content = result[0]
    image_content = result[1]
    assert "Generated:" in text_content.text
    assert str(tmp_path) in text_content.text
    # Verify workflow was built with empty prompt (indirectly via success)
    assert image_content.mimeType == "image/png"


@respx.mock
@pytest.mark.asyncio
async def test_generate_image_long_prompt(tmp_path, sample_image_bytes, mock_history_response, queue_empty, monkeypatch):
    """Very long prompt → passed as-is to workflow without truncation."""
    monkeypatch.setattr(server, "IMAGE_OUTPUT_DIR", str(tmp_path))

    long_prompt = "a " + "very long descriptive prompt " * 50  # ~500 chars

    respx.post(f"{COMFYUI_BASE}/api/prompt").mock(
        return_value=httpx.Response(200, json={"prompt_id": "test-long-uuid"})
    )
    respx.get(f"{COMFYUI_BASE}/api/queue").mock(
        return_value=httpx.Response(200, json=queue_empty)
    )
    mock_history_long = {
        "test-long-uuid": {
            "outputs": {
                "9": {
                    "images": [
                        {
                            "filename": "mcp-image-gen_00001_.png",
                            "subfolder": "",
                            "type": "output",
                        }
                    ]
                }
            },
            "status": {"completed": True},
        }
    }
    respx.get(f"{COMFYUI_BASE}/api/history/test-long-uuid").mock(
        return_value=httpx.Response(200, json=mock_history_long)
    )
    respx.get(f"{COMFYUI_BASE}/api/view").mock(
        return_value=httpx.Response(200, content=sample_image_bytes)
    )

    result = await generate_image(prompt=long_prompt, output_dir=str(tmp_path))

    assert len(result) == 2
    # Success implies long prompt was accepted (ComfyUI handles it)
    saved_files = list(tmp_path.glob("*.png"))
    assert len(saved_files) == 1


@respx.mock
@pytest.mark.asyncio
async def test_generate_image_invalid_model(tmp_path, monkeypatch):
    """Invalid model → ComfyUI /prompt returns 500 or 404, tool returns error TextContent."""
    monkeypatch.setattr(server, "IMAGE_OUTPUT_DIR", str(tmp_path))

    respx.post(f"{COMFYUI_BASE}/api/prompt").mock(
        return_value=httpx.Response(404, json={"error": "Model not found"})
    )

    result = await generate_image(
        prompt="a cat",
        model="nonexistent-model.safetensors",
        output_dir=str(tmp_path)
    )

    assert len(result) == 1
    assert "404" in result[0].text
    assert "Model not found" in result[0].text
    # No file saved
    saved_files = list(tmp_path.glob("*.png"))
    assert len(saved_files) == 0


@respx.mock
@pytest.mark.asyncio
async def test_generate_image_custom_output_dir(tmp_path, sample_image_bytes, mock_history_response, queue_empty, monkeypatch):
    """Custom output_dir → image saved there, path reflects it."""
    custom_dir = tmp_path / "custom"
    monkeypatch.setattr(server, "IMAGE_OUTPUT_DIR", str(tmp_path))  # Base for default

    respx.post(f"{COMFYUI_BASE}/api/prompt").mock(
        return_value=httpx.Response(200, json={"prompt_id": "test-custom-uuid"})
    )
    respx.get(f"{COMFYUI_BASE}/api/queue").mock(
        return_value=httpx.Response(200, json=queue_empty)
    )
    mock_history_custom = {
        "test-custom-uuid": {
            "outputs": {
                "9": {
                    "images": [
                        {
                            "filename": "mcp-image-gen_00001_.png",
                            "subfolder": "",
                            "type": "output",
                        }
                    ]
                }
            },
            "status": {"completed": True},
        }
    }
    respx.get(f"{COMFYUI_BASE}/api/history/test-custom-uuid").mock(
        return_value=httpx.Response(200, json=mock_history_custom)
    )
    respx.get(f"{COMFYUI_BASE}/api/view").mock(
        return_value=httpx.Response(200, content=sample_image_bytes)
    )

    result = await generate_image(
        prompt="a dog",
        output_dir=str(custom_dir),
    )

    assert len(result) == 2
    text_content = result[0]
    assert str(custom_dir) in text_content.text
    # Directory was created
    assert custom_dir.exists()
    saved_files = list(custom_dir.glob("*.png"))
    assert len(saved_files) == 1


@respx.mock
@pytest.mark.asyncio
async def test_generate_image_random_seed_variance(tmp_path, sample_image_bytes, mock_history_response, queue_empty, monkeypatch):
    """seed=-1 → different actual_seed each call, reflected in filename."""
    monkeypatch.setattr(server, "IMAGE_OUTPUT_DIR", str(tmp_path))

    # First generation
    respx.post(f"{COMFYUI_BASE}/api/prompt").mock(
        return_value=httpx.Response(200, json={"prompt_id": "seed1-uuid"})
    )
    respx.get(f"{COMFYUI_BASE}/api/queue").mock(
        return_value=httpx.Response(200, json=queue_empty)
    )
    mock_history_seed1 = {
        "seed1-uuid": {
            "outputs": {
                "9": {
                    "images": [
                        {
                            "filename": "mcp-image-gen_00001_.png",
                            "subfolder": "",
                            "type": "output",
                        }
                    ]
                }
            },
            "status": {"completed": True},
        }
    }
    respx.get(f"{COMFYUI_BASE}/api/history/seed1-uuid").mock(
        return_value=httpx.Response(200, json=mock_history_seed1)
    )
    respx.get(f"{COMFYUI_BASE}/api/view").mock(
        return_value=httpx.Response(200, content=sample_image_bytes)
    )

    result1 = await generate_image(prompt="cat", seed=-1, output_dir=str(tmp_path))
    seed1 = [line for line in result1[0].text.split("\n") if "Seed:" in line][0].split(": ")[1]
    filename1 = Path(result1[0].text.split("Generated: ")[1].split("\n")[0]).name
    assert "Seed:" in result1[0].text
    assert int(seed1) != 0  # Not default

    # Reset mocks for second call
    respx.reset()
    respx.post(f"{COMFYUI_BASE}/api/prompt").mock(
        return_value=httpx.Response(200, json={"prompt_id": "seed2-uuid"})
    )
    respx.get(f"{COMFYUI_BASE}/api/queue").mock(
        return_value=httpx.Response(200, json=queue_empty)
    )
    mock_history_seed2 = {
        "seed2-uuid": {
            "outputs": {
                "9": {
                    "images": [
                        {
                            "filename": "mcp-image-gen_00002_.png",
                            "subfolder": "",
                            "type": "output",
                        }
                    ]
                }
            },
            "status": {"completed": True},
        }
    }
    respx.get(f"{COMFYUI_BASE}/api/history/seed2-uuid").mock(
        return_value=httpx.Response(200, json=mock_history_seed2)
    )
    respx.get(f"{COMFYUI_BASE}/api/view").mock(
        return_value=httpx.Response(200, content=sample_image_bytes)
    )

    result2 = await generate_image(prompt="cat", seed=-1, output_dir=str(tmp_path))
    seed2 = [line for line in result2[0].text.split("\n") if "Seed:" in line][0].split(": ")[1]
    filename2 = Path(result2[0].text.split("Generated: ")[1].split("\n")[0]).name

    # Different seeds and filenames
    assert seed1 != seed2
    assert filename1 != filename2
    # Both saved
    saved_files = list(tmp_path.glob("*.png"))
    assert len(saved_files) == 2
