from pathlib import Path
import subprocess

import tools.manual_multimodal_test_client as manual_multimodal_test_client

from tools.manual_multimodal_test_client import (
    MODE_DEFAULTS,
    _download_artifact,
    build_request_spec,
    build_parser,
    download_default_sample,
    choose_mode_interactively,
    ensure_sample_file,
    execute_request,
    handle_response_payload,
    parse_args,
    resolve_mode_config,
)

import pytest


class _MainExit(Exception):
    pass


def test_parse_args_accepts_direct_mode_and_global_overrides():
    args = parse_args([
        "llm",
        "--url", "http://127.0.0.1:6745",
        "--token", "secret-token",
        "--model", "text:test-model",
        "--prompt", "Say hello",
        "--output-dir", "tmp/out",
    ])

    assert args.mode == "llm"
    assert args.url == "http://127.0.0.1:6745"
    assert args.token == "secret-token"
    assert args.model == "text:test-model"
    assert args.prompt == "Say hello"
    assert args.output_dir == "tmp/out"


def test_choose_mode_interactively_maps_numeric_selection(monkeypatch):
    monkeypatch.setattr("builtins.input", lambda _: "2")

    mode = choose_mode_interactively()

    assert mode == "transcription"


def test_parse_args_leaves_mode_empty_for_interactive_fallback():
    args = parse_args([])

    assert args.mode is None


def test_parse_args_rejects_generic_file_argument():
    try:
        parse_args(["llm", "--file", "sample.dat"])
    except SystemExit as exc:
        assert exc.code == 2
    else:
        raise AssertionError("parse_args accepted deprecated --file argument")


def test_resolve_mode_config_uses_mode_defaults_when_overrides_absent(tmp_path):
    args = parse_args(["audio-generation", "--output-dir", str(tmp_path)])

    config = resolve_mode_config(args, selected_mode="audio-generation")

    assert config["mode"] == "audio-generation"
    assert config["url"] == "http://127.0.0.1:6745"
    assert config["model"] == MODE_DEFAULTS["audio-generation"]["model"]
    assert config["prompt"] == MODE_DEFAULTS["audio-generation"]["prompt"]
    assert config["response_format"] == MODE_DEFAULTS["audio-generation"]["response_format"]
    assert config["output_dir"] == tmp_path
    assert "file" not in config


def test_main_uses_fastapi_app_instance_for_state_and_server(monkeypatch):
    import codai.main as main_module
    from codai.api import app as app_module

    fake_args = type("Args", (), {
        "config": "/tmp/coderai-test",
        "dump": False,
        "debug": False,
        "list_models": False,
        "list_devices": False,
        "list_cached_models": False,
        "remove_all_models": False,
        "remove_model": None,
        "download_model": None,
        "download_file_pattern": None,
        "vulkan_list_devices": False,
        "download_gguf": None,
    })()
    fake_server = type("ServerCfg", (), {
        "host": "127.0.0.1",
        "port": 8765,
        "https": False,
        "https_key_path": None,
        "https_cert_path": None,
        "queue_max_size": 1,
        "max_parallel_requests": 1,
    })()
    fake_backend = type("BackendCfg", (), {"type": "vulkan"})()
    fake_config = type("Config", (), {
        "server": fake_server,
        "backend": fake_backend,
        "system_prompt": "",
        "tools_closer_prompt": "",
        "grammar_guided": False,
        "file_path": "/tmp",
        "models": type("ModelsCfg", (), {"load_mode": "ondemand", "default_load_mode": "ondemand", "hf_cache_dir": None, "gguf_cache_dir": None})(),
        "offload": type("OffloadCfg", (), {
            "directory": "/tmp",
            "manual_ram_gb": None,
            "strategy": "auto",
            "no_ram": False,
            "load_in_4bit": False,
            "load_in_8bit": False,
            "flash_attention": False,
            "max_gpu_percent": None,
        })(),
        "image": type("ImageCfg", (), {
            "sample_method": "res_multistep",
            "steps": 4,
            "width": 512,
            "height": 512,
            "cfg_scale": 1.0,
            "precision": "f32",
            "cpu_offload": False,
            "seed": None,
            "vae_tiling": False,
            "clip_on_cpu": False,
        })(),
        "parser": None,
        "hf_chat_templates": None,
        "reasoning_options": None,
        "whisper": type("WhisperCfg", (), {"server_path": None, "server_port": 8744})(),
        "vulkan": type("VulkanCfg", (), {"n_gpu_layers": -1, "n_ctx": 2048, "device_id": 0, "single_gpu": False})(),
        "nvidia": type("NvidiaCfg", (), {})(),
    })()
    fake_config_mgr = type("ConfigMgr", (), {
        "models_data": {},
        "pipelines_data": [],
    })()

    monkeypatch.setattr(main_module, "parse_args", lambda: fake_args)
    monkeypatch.setattr(main_module, "ConfigManager", lambda path: type("CfgMgr", (), {
        "load": lambda self: fake_config,
        "models_data": {},
        "pipelines_data": [],
    })())
    monkeypatch.setattr(main_module, "init_session_manager", lambda *args, **kwargs: None)
    monkeypatch.setattr(main_module, "set_config_manager", lambda mgr: None)
    monkeypatch.setattr("codai.backends.detect_available_backends", lambda: {"vulkan": True})
    monkeypatch.setattr("codai.api.state.set_global_args", lambda *args, **kwargs: None)
    monkeypatch.setattr("codai.api.state.set_global_debug", lambda *args, **kwargs: None)
    monkeypatch.setattr("codai.api.state.set_global_system_prompt", lambda *args, **kwargs: None, raising=False)
    monkeypatch.setattr("codai.api.state.set_global_tools_closer_prompt", lambda *args, **kwargs: None, raising=False)
    monkeypatch.setattr("codai.api.state.set_global_file_path", lambda *args, **kwargs: None)
    monkeypatch.setattr("codai.api.state.set_load_mode", lambda *args, **kwargs: None)
    monkeypatch.setattr("codai.api.state.set_grammar_guided_gen", lambda *args, **kwargs: None)
    monkeypatch.setattr("codai.api.text.set_global_args", lambda *args, **kwargs: None)
    monkeypatch.setattr("codai.api.text.set_global_debug", lambda *args, **kwargs: None)
    monkeypatch.setattr("codai.api.text.set_global_system_prompt", lambda *args, **kwargs: None)
    monkeypatch.setattr("codai.api.text.set_global_tools_closer_prompt", lambda *args, **kwargs: None)
    monkeypatch.setattr("codai.api.app.set_load_mode", lambda *args, **kwargs: None)
    monkeypatch.setattr("codai.api.app.set_global_file_path_wrapper", lambda *args, **kwargs: None)
    monkeypatch.setattr("codai.api.images.set_global_file_path", lambda *args, **kwargs: None)
    monkeypatch.setattr("codai.api.images.set_global_args", lambda *args, **kwargs: None)
    monkeypatch.setattr("codai.api.video.set_global_args", lambda *args, **kwargs: None)
    monkeypatch.setattr("codai.api.video.set_global_file_path", lambda *args, **kwargs: None)
    monkeypatch.setattr("codai.api.audio_gen.set_global_args", lambda *args, **kwargs: None)
    monkeypatch.setattr("codai.api.audio_gen.set_global_file_path", lambda *args, **kwargs: None)
    monkeypatch.setattr("codai.api.audio_stems.set_global_args", lambda *args, **kwargs: None)
    monkeypatch.setattr("codai.api.audio_stems.set_global_file_path", lambda *args, **kwargs: None)
    monkeypatch.setattr("codai.api.audio_clean.set_global_args", lambda *args, **kwargs: None)
    monkeypatch.setattr("codai.api.audio_clean.set_global_file_path", lambda *args, **kwargs: None)
    monkeypatch.setattr("codai.api.voice_clone.set_global_args", lambda *args, **kwargs: None)
    monkeypatch.setattr("codai.api.voice_clone.set_global_file_path", lambda *args, **kwargs: None)
    monkeypatch.setattr("codai.api.voice_convert.set_global_args", lambda *args, **kwargs: None)
    monkeypatch.setattr("codai.api.voice_convert.set_global_file_path", lambda *args, **kwargs: None)
    monkeypatch.setattr("codai.api.faceswap.set_global_args", lambda *args, **kwargs: None)
    monkeypatch.setattr("codai.api.faceswap.set_global_file_path", lambda *args, **kwargs: None)
    monkeypatch.setattr("codai.api.characters.set_global_args", lambda *args, **kwargs: None)
    monkeypatch.setattr("codai.api.embeddings.set_global_args", lambda *args, **kwargs: None)
    monkeypatch.setattr("codai.models.manager.model_manager", type("MM", (), {"backend": None, "backend_type": None})())
    monkeypatch.setattr("codai.models.manager.multi_model_manager", type("MMM", (), {"set_load_mode": lambda self, mode: None, "list_models": lambda self: []})())
    monkeypatch.setattr("codai.queue.manager.queue_manager", type("Q", (), {"max_size": 0, "max_parallel_requests": 0})())

    captured = {}

    def fake_run(target, host, port, ssl_context=None):
        captured["target"] = target
        captured["host"] = host
        captured["port"] = port
        raise _MainExit()

    monkeypatch.setattr("uvicorn.run", fake_run)

    try:
        main_module.main()
    except _MainExit:
        pass

    assert captured["target"] is app_module.app
    assert app_module.app.state.config is fake_config


def test_resolve_mode_config_normalizes_url_and_uses_default_input_files(tmp_path):
    args = parse_args([
        "transcription",
        "--url", "http://127.0.0.1:6745/",
        "--output-dir", str(tmp_path),
    ])

    config = resolve_mode_config(args, selected_mode="transcription")

    assert config["url"] == "http://127.0.0.1:6745"
    assert config["audio_file"] == MODE_DEFAULTS["transcription"]["audio_file"]


def test_resolve_mode_config_uses_default_video_file_for_video_doubt(tmp_path):
    args = parse_args(["video-doubt", "--output-dir", str(tmp_path)])

    config = resolve_mode_config(args, selected_mode="video-doubt")

    assert config["video_file"] == MODE_DEFAULTS["video-doubt"]["video_file"]


def test_resolve_mode_config_prefers_explicit_model_prompt_url_and_response_format_overrides(tmp_path):
    args = parse_args([
        "video-generation",
        "--url", "http://example.invalid:9999/",
        "--model", "video:custom-model",
        "--prompt", "Custom prompt",
        "--response-format", "b64_json",
        "--output-dir", str(tmp_path),
    ])

    config = resolve_mode_config(args, selected_mode="video-generation")

    assert config["url"] == "http://example.invalid:9999"
    assert config["model"] == "video:custom-model"
    assert config["prompt"] == "Custom prompt"
    assert config["response_format"] == "b64_json"
    assert config["output_dir"] == tmp_path


def test_resolve_mode_config_keeps_explicit_empty_string_overrides(tmp_path):
    args = parse_args([
        "video-generation",
        "--model", "",
        "--audio-file", "",
        "--video-file", "",
        "--response-format", "",
        "--output-dir", str(tmp_path),
    ])

    config = resolve_mode_config(args, selected_mode="video-generation")

    assert config["model"] == ""
    assert config["audio_file"] == ""
    assert config["video_file"] == ""
    assert config["response_format"] == ""


def test_ensure_sample_file_returns_existing_path_without_download(tmp_path, monkeypatch):
    sample_path = tmp_path / "samples" / "transcription.wav"
    sample_path.parent.mkdir(parents=True)
    sample_path.write_bytes(b"wav-bytes")
    calls = []

    monkeypatch.setattr(
        "tools.manual_multimodal_test_client.download_default_sample",
        lambda path: calls.append(path),
    )

    result = ensure_sample_file(str(sample_path), "--audio-file")

    assert result == sample_path
    assert calls == []


def test_ensure_sample_file_downloads_missing_managed_default(tmp_path, monkeypatch):
    managed_path = tmp_path / "samples" / "question-audio.wav"
    downloaded = []

    def fake_download(path):
        downloaded.append(path)
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_bytes(b"audio")
        return path

    monkeypatch.setattr(
        "tools.manual_multimodal_test_client.SAMPLE_URLS",
        {"samples/question-audio.wav": "https://example.invalid/question-audio.wav"},
    )
    monkeypatch.setattr(
        "tools.manual_multimodal_test_client.download_default_sample",
        fake_download,
    )

    result = ensure_sample_file(str(managed_path), "--audio-file")

    assert result == managed_path
    assert downloaded == [managed_path]
    assert managed_path.read_bytes() == b"audio"


def test_ensure_sample_file_downloads_missing_managed_default_for_equivalent_path(tmp_path, monkeypatch):
    project_root = tmp_path / "project"
    project_root.mkdir()
    managed_path = project_root / "samples" / "transcription.wav"
    downloaded = []

    def fake_download(path):
        downloaded.append(path)
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_bytes(b"wav")
        return path

    monkeypatch.setattr(
        "tools.manual_multimodal_test_client.SAMPLE_URLS",
        {"samples/transcription.wav": "https://example.invalid/transcription.wav"},
    )
    monkeypatch.chdir(project_root)
    monkeypatch.setattr(
        "tools.manual_multimodal_test_client.download_default_sample",
        fake_download,
    )

    result = ensure_sample_file("./samples/transcription.wav", "--audio-file")

    assert result == Path("samples/transcription.wav")
    assert downloaded == [Path("samples/transcription.wav")]
    assert managed_path.read_bytes() == b"wav"


def test_ensure_sample_file_downloads_missing_managed_default_for_absolute_path(tmp_path, monkeypatch):
    project_root = tmp_path / "project"
    managed_path = project_root / "samples" / "transcription.wav"
    downloaded = []

    def fake_download(path):
        downloaded.append(path)
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_bytes(b"wav")
        return path

    monkeypatch.setattr(
        "tools.manual_multimodal_test_client.SAMPLE_URLS",
        {"samples/transcription.wav": "https://example.invalid/transcription.wav"},
    )
    monkeypatch.setattr(
        "tools.manual_multimodal_test_client.download_default_sample",
        fake_download,
    )

    result = ensure_sample_file(str(managed_path), "--audio-file")

    assert result == managed_path
    assert downloaded == [managed_path]
    assert managed_path.read_bytes() == b"wav"


def test_ensure_sample_file_rejects_missing_explicit_override(tmp_path):
    missing_path = tmp_path / "custom.wav"

    with pytest.raises(FileNotFoundError, match=rf"File not found: {missing_path}\. Supply --audio-file\."):
        ensure_sample_file(str(missing_path), "--audio-file")


def test_download_default_sample_runs_wget_and_returns_path(tmp_path, monkeypatch):
    sample_path = tmp_path / "samples" / "transcription.wav"
    recorded = {}

    def fake_run(command, check):
        recorded["command"] = command
        recorded["check"] = check
        sample_path.parent.mkdir(parents=True, exist_ok=True)
        sample_path.write_bytes(b"downloaded-wav")

    monkeypatch.setattr(
        "tools.manual_multimodal_test_client.SAMPLE_URLS",
        {"samples/transcription.wav": "https://example.invalid/transcription.wav"},
    )
    monkeypatch.setattr(manual_multimodal_test_client.subprocess, "run", fake_run, raising=False)

    result = download_default_sample(sample_path)

    assert result == sample_path
    assert recorded["command"] == [
        "wget",
        "-O",
        str(sample_path),
        "https://example.invalid/transcription.wav",
    ]
    assert recorded["check"] is True
    assert sample_path.read_bytes() == b"downloaded-wav"


def test_download_default_sample_wraps_wget_failure(tmp_path, monkeypatch):
    sample_path = tmp_path / "samples" / "question-video.mp4"

    def fake_run(command, check):
        raise subprocess.CalledProcessError(returncode=1, cmd=command)

    monkeypatch.setattr(
        "tools.manual_multimodal_test_client.SAMPLE_URLS",
        {"samples/question-video.mp4": "https://example.invalid/question-video.mp4"},
    )
    monkeypatch.setattr(manual_multimodal_test_client.subprocess, "run", fake_run, raising=False)

    with pytest.raises(FileNotFoundError, match=r"Unable to download required default file"):
        download_default_sample(sample_path)


def test_build_request_spec_for_llm_uses_chat_completions_payload(tmp_path):
    config = {
        "mode": "llm",
        "url": "http://127.0.0.1:6745",
        "model": "text:test",
        "prompt": "Ping",
        "output_dir": tmp_path,
        "token": None,
        "audio_file": None,
        "video_file": None,
        "response_format": None,
    }

    spec = build_request_spec(config)

    assert spec == {
        "method": "POST",
        "url": "http://127.0.0.1:6745/v1/chat/completions",
        "headers": {"Accept": "application/json"},
        "json": {
            "model": "text:test",
            "messages": [{"role": "user", "content": "Ping"}],
        },
    }


def test_build_request_spec_for_transcription_uses_multipart_file(tmp_path):
    audio_path = tmp_path / "sample.wav"
    audio_path.write_bytes(b"wav-bytes")
    config = {
        "mode": "transcription",
        "url": "http://127.0.0.1:6745",
        "model": "audio:test",
        "prompt": "Transcribe carefully",
        "output_dir": tmp_path,
        "token": None,
        "audio_file": str(audio_path),
        "video_file": None,
        "response_format": None,
    }

    spec = build_request_spec(config)

    assert spec["method"] == "POST"
    assert spec["url"] == "http://127.0.0.1:6745/v1/audio/transcriptions"
    assert spec["headers"] == {"Accept": "application/json"}
    assert spec["data"] == {
        "model": "audio:test",
        "prompt": "Transcribe carefully",
    }
    uploaded_name, uploaded_file = spec["files"]["file"]
    assert uploaded_name == "sample.wav"
    assert uploaded_file.read() == b"wav-bytes"
    assert uploaded_file.closed is False
    uploaded_file.close()


def test_build_request_spec_for_transcription_downloads_missing_managed_default(tmp_path, monkeypatch):
    managed_path = tmp_path / "samples" / "transcription.wav"
    downloaded = []
    config = {
        "mode": "transcription",
        "url": "http://127.0.0.1:6745",
        "model": "audio:test",
        "prompt": "Transcribe carefully",
        "output_dir": tmp_path,
        "token": None,
        "audio_file": str(managed_path),
        "video_file": None,
        "response_format": None,
    }

    def fake_download(path):
        downloaded.append(path)
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_bytes(b"downloaded-wav")
        return path

    monkeypatch.setattr(
        "tools.manual_multimodal_test_client.SAMPLE_URLS",
        {"samples/transcription.wav": "https://example.invalid/transcription.wav"},
    )
    monkeypatch.setattr(
        "tools.manual_multimodal_test_client.download_default_sample",
        fake_download,
    )

    spec = build_request_spec(config)

    uploaded_name, uploaded_file = spec["files"]["file"]
    assert uploaded_name == "transcription.wav"
    assert uploaded_file.read() == b"downloaded-wav"
    assert downloaded == [managed_path]
    uploaded_file.close()


def test_build_request_spec_for_transcription_downloads_missing_default_audio(tmp_path, monkeypatch):
    managed_path = tmp_path / "samples" / "transcription.wav"
    calls = []
    config = {
        "mode": "transcription",
        "url": "http://127.0.0.1:6745",
        "model": "audio:test",
        "prompt": "Transcribe carefully",
        "output_dir": tmp_path,
        "token": None,
        "audio_file": str(managed_path),
        "video_file": None,
        "response_format": None,
    }

    def fake_ensure(path_value, flag_name):
        calls.append((path_value, flag_name))
        managed_path.parent.mkdir(parents=True, exist_ok=True)
        managed_path.write_bytes(b"downloaded-audio")
        return managed_path

    monkeypatch.setattr(
        "tools.manual_multimodal_test_client.ensure_sample_file",
        fake_ensure,
    )

    spec = build_request_spec(config)

    uploaded_name, uploaded_file = spec["files"]["file"]
    assert calls == [(str(managed_path), "--audio-file")]
    assert uploaded_name == "transcription.wav"
    assert uploaded_file.read() == b"downloaded-audio"
    uploaded_file.close()


def test_build_request_spec_for_audio_generation_uses_json_payload(tmp_path):
    config = {
        "mode": "audio-generation",
        "url": "http://127.0.0.1:6745",
        "model": "audio_gen:test",
        "prompt": "Generate a bell sound",
        "output_dir": tmp_path,
        "token": None,
        "audio_file": None,
        "video_file": None,
        "response_format": "url",
    }

    spec = build_request_spec(config)

    assert spec == {
        "method": "POST",
        "url": "http://127.0.0.1:6745/v1/audio/generate",
        "headers": {"Accept": "application/json"},
        "json": {
            "model": "audio_gen:test",
            "prompt": "Generate a bell sound",
            "response_format": "url",
        },
    }


def test_build_request_spec_for_video_generation_uses_json_payload(tmp_path):
    config = {
        "mode": "video-generation",
        "url": "http://127.0.0.1:6745",
        "model": "video:test",
        "prompt": "Generate a short test clip",
        "output_dir": tmp_path,
        "token": None,
        "audio_file": None,
        "video_file": None,
        "response_format": "url",
    }

    spec = build_request_spec(config)

    assert spec == {
        "method": "POST",
        "url": "http://127.0.0.1:6745/v1/video/generations",
        "headers": {"Accept": "application/json"},
        "json": {
            "model": "video:test",
            "prompt": "Generate a short test clip",
            "response_format": "url",
        },
    }


def test_build_request_spec_for_video_doubt_request_builder_uses_text_endpoint_with_video_context(tmp_path):
    video_path = tmp_path / "clip.mp4"
    video_path.write_bytes(b"video-bytes")
    config = {
        "mode": "video-doubt",
        "url": "http://127.0.0.1:6745",
        "model": "vision:test",
        "prompt": "What happens in this clip?",
        "output_dir": tmp_path,
        "token": None,
        "audio_file": None,
        "video_file": str(video_path),
        "response_format": None,
    }

    spec = build_request_spec(config)

    assert spec["url"].endswith("/v1/chat/completions")
    assert spec["json"]["model"] == "vision:test"
    assert str(video_path) in spec["json"]["messages"][0]["content"]
    assert "What happens in this clip?" in spec["json"]["messages"][0]["content"]
    assert "local path reference only" in spec["json"]["messages"][0]["content"]
    assert "may or may not support reasoning from that reference" in spec["json"]["messages"][0]["content"]
    assert "acknowledge that limitation" in spec["json"]["messages"][0]["content"]


def test_build_request_spec_for_video_doubt_downloads_missing_default_video(tmp_path, monkeypatch):
    managed_path = tmp_path / "samples" / "question-video.mp4"
    calls = []
    config = {
        "mode": "video-doubt",
        "url": "http://127.0.0.1:6745",
        "model": "vision:test",
        "prompt": "What happens in this clip?",
        "output_dir": tmp_path,
        "token": None,
        "audio_file": None,
        "video_file": str(managed_path),
        "response_format": None,
    }

    def fake_ensure(path_value, flag_name):
        calls.append((path_value, flag_name))
        managed_path.parent.mkdir(parents=True, exist_ok=True)
        managed_path.write_bytes(b"video-bytes")
        return managed_path

    monkeypatch.setattr(
        "tools.manual_multimodal_test_client.ensure_sample_file",
        fake_ensure,
    )

    spec = build_request_spec(config)

    assert calls == [(str(managed_path), "--video-file")]
    assert str(managed_path) in spec["json"]["messages"][0]["content"]
    assert "What happens in this clip?" in spec["json"]["messages"][0]["content"]


def test_build_request_spec_for_music_audio_doubt_request_builder_uses_text_endpoint_with_audio_context(tmp_path):
    audio_path = tmp_path / "clip.wav"
    audio_path.write_bytes(b"audio-bytes")
    config = {
        "mode": "music-audio-doubt",
        "url": "http://127.0.0.1:6745",
        "model": "audio:test",
        "prompt": "Describe the music.",
        "output_dir": tmp_path,
        "token": None,
        "audio_file": str(audio_path),
        "video_file": None,
        "response_format": None,
    }

    spec = build_request_spec(config)

    assert spec["url"].endswith("/v1/chat/completions")
    assert spec["json"]["model"] == "audio:test"
    assert str(audio_path) in spec["json"]["messages"][0]["content"]
    assert "Describe the music." in spec["json"]["messages"][0]["content"]
    assert "local path reference only" in spec["json"]["messages"][0]["content"]
    assert "may or may not support reasoning from that reference" in spec["json"]["messages"][0]["content"]
    assert "acknowledge that limitation" in spec["json"]["messages"][0]["content"]


def test_build_request_spec_for_video_doubt_request_builder_requires_video_file_flag(tmp_path):
    config = {
        "mode": "video-doubt",
        "url": "http://127.0.0.1:6745",
        "model": "vision:test",
        "prompt": "What happens in this clip?",
        "output_dir": tmp_path,
        "token": None,
        "audio_file": None,
        "video_file": None,
        "response_format": None,
    }

    with pytest.raises(FileNotFoundError, match=r"Missing required file\. Supply --video-file\."):
        build_request_spec(config)


def test_build_request_spec_for_music_audio_doubt_request_builder_requires_audio_file_flag(tmp_path):
    config = {
        "mode": "music-audio-doubt",
        "url": "http://127.0.0.1:6745",
        "model": "audio:test",
        "prompt": "Describe the music.",
        "output_dir": tmp_path,
        "token": None,
        "audio_file": None,
        "video_file": None,
        "response_format": None,
    }

    with pytest.raises(FileNotFoundError, match=r"Missing required file\. Supply --audio-file\."):
        build_request_spec(config)


def test_build_request_spec_for_transcription_requires_audio_file_flag(tmp_path):
    config = {
        "mode": "transcription",
        "url": "http://127.0.0.1:6745",
        "model": "audio:test",
        "prompt": "Transcribe carefully",
        "output_dir": tmp_path,
        "token": None,
        "audio_file": None,
        "video_file": None,
        "response_format": None,
    }

    with pytest.raises(FileNotFoundError, match=r"Missing required file\. Supply --audio-file\."):
        build_request_spec(config)


def test_build_request_spec_for_transcription_requires_existing_audio_file(tmp_path):
    missing_path = tmp_path / "missing.wav"
    config = {
        "mode": "transcription",
        "url": "http://127.0.0.1:6745",
        "model": "audio:test",
        "prompt": "Transcribe carefully",
        "output_dir": tmp_path,
        "token": None,
        "audio_file": str(missing_path),
        "video_file": None,
        "response_format": None,
    }

    with pytest.raises(FileNotFoundError, match=rf"File not found: {missing_path}\. Supply --audio-file\."):
        build_request_spec(config)


class DummyResponse:
    def __init__(self, payload, status_code=200):
        self._payload = payload
        self.status_code = status_code
        self.text = "payload-text"

    def json(self):
        return self._payload

    def raise_for_status(self):
        if self.status_code >= 400:
            raise RuntimeError(f"HTTP {self.status_code}")


class DummyDownloadResponse:
    def __init__(self, content, status_code=200):
        self.content = content
        self.status_code = status_code

    def raise_for_status(self):
        if self.status_code >= 400:
            raise RuntimeError(f"HTTP {self.status_code}")


def test_task5_download_artifact_returns_response_content(monkeypatch):
    monkeypatch.setattr(
        "tools.manual_multimodal_test_client.requests.get",
        lambda url, timeout: DummyDownloadResponse(b"artifact-bytes"),
    )

    result = _download_artifact("http://example.invalid/artifact.bin")

    assert result == b"artifact-bytes"


def test_task5_handle_response_payload_returns_llm_text_without_artifact(tmp_path):
    payload = {
        "choices": [{"message": {"content": "hello from model"}}]
    }
    response = DummyResponse(payload)

    result = handle_response_payload("llm", response, tmp_path)

    assert result["text"] == "hello from model"
    assert result["artifact_path"] is None
    assert result["payload"] == payload


def test_task5_handle_response_payload_flattens_structured_chat_content(tmp_path):
    payload = {
        "choices": [{
            "message": {
                "content": [
                    {"type": "text", "text": "hello"},
                    {"type": "input_text", "text": "from model"},
                    {"type": "tool_result", "value": 7},
                ]
            }
        }]
    }
    response = DummyResponse(payload)

    result = handle_response_payload("llm", response, tmp_path)

    assert result["text"] == 'hello\nfrom model\n{"type": "tool_result", "value": 7}'
    assert result["artifact_path"] is None
    assert result["payload"] == payload


def test_task5_handle_response_payload_downloads_url_artifact(monkeypatch, tmp_path):
    payload = {
        "data": [{"url": "http://example.invalid/audio.wav", "text": "generated audio summary"}]
    }
    response = DummyResponse(payload)
    monkeypatch.setattr(
        "tools.manual_multimodal_test_client._download_artifact",
        lambda url: b"wave-bytes",
    )

    result = handle_response_payload("audio-generation", response, tmp_path)

    assert result["artifact_path"].suffix == ".wav"
    assert result["artifact_path"].read_bytes() == b"wave-bytes"
    assert result["text"] == "generated audio summary"
    assert result["payload"] == payload


def test_task5_handle_response_payload_decodes_base64_artifact(tmp_path):
    payload = {
        "data": [{"b64_json": "aGVsbG8=", "caption": "inline audio artifact"}]
    }
    response = DummyResponse(payload)

    result = handle_response_payload("audio-generation", response, tmp_path)

    assert result["artifact_path"].read_bytes() == b"hello"
    assert result["text"] == "inline audio artifact"
    assert result["payload"] == payload


def test_task5_handle_response_payload_downloads_video_generation_artifact_as_mp4(monkeypatch, tmp_path):
    payload = {
        "data": [{"url": "http://example.invalid/video.mp4", "caption": "generated video summary"}]
    }
    response = DummyResponse(payload)
    monkeypatch.setattr(
        "tools.manual_multimodal_test_client._download_artifact",
        lambda url: b"video-bytes",
    )

    result = handle_response_payload("video-generation", response, tmp_path)

    assert result["artifact_path"].suffix == ".mp4"
    assert result["artifact_path"].read_bytes() == b"video-bytes"
    assert result["text"] == "generated video summary"
    assert result["payload"] == payload


def test_task5_handle_response_payload_propagates_http_errors(tmp_path):
    response = DummyResponse({"error": "bad request"}, status_code=500)

    with pytest.raises(RuntimeError, match=r"HTTP 500"):
        handle_response_payload("llm", response, tmp_path)


def test_task5_handle_response_payload_rejects_generation_without_artifact_fields(tmp_path):
    payload = {
        "data": [{"text": "generated audio summary"}]
    }
    response = DummyResponse(payload)

    with pytest.raises(ValueError, match=r"No artifact found in generation response for mode: audio-generation"):
        handle_response_payload("audio-generation", response, tmp_path)


def test_task5_execute_request_forwards_method_url_timeout_and_kwargs(monkeypatch):
    captured = {}

    def fake_request(*, method, url, timeout, **kwargs):
        captured["method"] = method
        captured["url"] = url
        captured["timeout"] = timeout
        captured["kwargs"] = kwargs
        return "response-object"

    monkeypatch.setattr("tools.manual_multimodal_test_client.requests.request", fake_request)

    result = execute_request({
        "method": "POST",
        "url": "http://127.0.0.1:6745/v1/audio/generate",
        "headers": {"Accept": "application/json"},
        "json": {"prompt": "Ping"},
    })

    assert result == "response-object"
    assert captured == {
        "method": "POST",
        "url": "http://127.0.0.1:6745/v1/audio/generate",
        "timeout": 300,
        "kwargs": {
            "headers": {"Accept": "application/json"},
            "json": {"prompt": "Ping"},
        },
    }


def test_task5_execute_request_filters_method_and_url_from_forwarded_kwargs(monkeypatch):
    captured = {}

    def fake_request(*, method, url, timeout, **kwargs):
        captured["method"] = method
        captured["url"] = url
        captured["timeout"] = timeout
        captured["kwargs"] = kwargs
        return "response-object"

    monkeypatch.setattr("tools.manual_multimodal_test_client.requests.request", fake_request)

    result = execute_request({
        "method": "GET",
        "url": "http://127.0.0.1:6745/health",
        "params": {"verbose": "1"},
    })

    assert result == "response-object"
    assert captured["method"] == "GET"
    assert captured["url"] == "http://127.0.0.1:6745/health"
    assert captured["timeout"] == 300
    assert captured["kwargs"] == {"params": {"verbose": "1"}}
    assert "method" not in captured["kwargs"]
    assert "url" not in captured["kwargs"]


def test_main_runs_selected_mode_and_prints_text(monkeypatch, capsys, tmp_path):
    monkeypatch.setattr("tools.manual_multimodal_test_client.choose_mode_interactively", lambda: "llm")
    monkeypatch.setattr(
        "tools.manual_multimodal_test_client.execute_mode",
        lambda args, selected_mode: {"text": "done", "artifact_path": None},
    )

    from tools.manual_multimodal_test_client import main

    exit_code = main(["--output-dir", str(tmp_path)])
    captured = capsys.readouterr()

    assert exit_code == 0
    assert captured.out == "done\n"


def test_main_prints_artifact_path_when_present(monkeypatch, capsys, tmp_path):
    artifact = tmp_path / "out.wav"
    artifact.write_bytes(b"bytes")
    monkeypatch.setattr(
        "tools.manual_multimodal_test_client.execute_mode",
        lambda args, selected_mode: {"text": "summary", "artifact_path": artifact},
    )

    from tools.manual_multimodal_test_client import main

    exit_code = main(["audio-generation", "--output-dir", str(tmp_path)])
    captured = capsys.readouterr()

    assert exit_code == 0
    assert captured.out == f"summary\n{artifact}\n"


def test_main_returns_error_when_interactive_mode_selection_fails(monkeypatch, capsys, tmp_path):
    monkeypatch.setattr(
        "tools.manual_multimodal_test_client.choose_mode_interactively",
        lambda: (_ for _ in ()).throw(ValueError("Invalid mode selection: 99")),
    )

    from tools.manual_multimodal_test_client import main

    exit_code = main(["--output-dir", str(tmp_path)])
    captured = capsys.readouterr()

    assert exit_code == 1
    assert captured.out == "ERROR [interactive]: Invalid mode selection: 99\n"
