import pytest
from pydantic import ValidationError

from nexus_mcp.types import (
    MAX_PROMPT_LENGTH,
    AgentResponse,
    AgentTask,
    AgentTaskResult,
    MultiPromptResponse,
    PromptRequest,
    SessionPreferences,
    SubprocessResult,
)


def test_prompt_request_valid():
    req = PromptRequest(cli="gemini", prompt="Hello")
    assert req.cli == "gemini"
    assert req.prompt == "Hello"
    assert req.context == {}


def test_prompt_request_empty_prompt_fails():
    with pytest.raises(ValidationError):
        PromptRequest(cli="gemini", prompt="")


def test_prompt_request_empty_agent_rejected():
    """Empty agent string is rejected by min_length=1."""
    with pytest.raises(ValidationError):
        PromptRequest(cli="", prompt="hi")


def test_prompt_request_default_execution_mode():
    req = PromptRequest(cli="gemini", prompt="Hello")
    assert req.execution_mode == "default"


def test_prompt_request_yolo_mode():
    req = PromptRequest(cli="gemini", prompt="Hello", execution_mode="yolo")
    assert req.execution_mode == "yolo"


def test_prompt_request_invalid_execution_mode():
    with pytest.raises(ValidationError):
        PromptRequest(cli="gemini", prompt="Hello", execution_mode="invalid")


def test_agent_response_structure():
    resp = AgentResponse(
        cli="gemini", output="test output", raw_output='{"response": "test output"}'
    )
    assert resp.cli == "gemini"
    assert resp.output == "test output"
    assert resp.metadata == {}


def test_subprocess_result_structure():
    result = SubprocessResult(stdout="output", stderr="", returncode=0)
    assert result.stdout == "output"
    assert result.stderr == ""
    assert result.returncode == 0


def test_subprocess_result_nonzero_returncode():
    result = SubprocessResult(stdout="", stderr="error", returncode=1)
    assert result.returncode == 1
    assert result.stderr == "error"


def test_prompt_request_with_model():
    """Model field should accept valid model names."""
    req = PromptRequest(cli="gemini", prompt="Hello", model="gemini-2.5-flash")
    assert req.model == "gemini-2.5-flash"


def test_prompt_request_default_model_is_none():
    """Model field should default to None (uses CLI default)."""
    req = PromptRequest(cli="gemini", prompt="Hello")
    assert req.model is None


def test_prompt_request_empty_string_model_fails():
    """Empty string should fail validation (prevent malformed commands)."""
    with pytest.raises(ValidationError):
        PromptRequest(cli="gemini", prompt="Hello", model="")


# Phase 3.7: File References tests
def test_prompt_request_with_file_refs():
    """PromptRequest accepts optional file_refs list."""
    request = PromptRequest(
        cli="gemini", prompt="Analyze this code", file_refs=["src/main.py", "tests/test_main.py"]
    )
    assert request.file_refs == ["src/main.py", "tests/test_main.py"]


def test_prompt_request_file_refs_default_empty():
    """PromptRequest.file_refs defaults to empty list."""
    request = PromptRequest(cli="gemini", prompt="test")
    assert request.file_refs == []


def test_prompt_request_file_refs_must_be_strings():
    """PromptRequest.file_refs must contain only strings."""
    with pytest.raises(ValidationError):
        PromptRequest(
            cli="gemini",
            prompt="test",
            file_refs=["valid.py", 123, None],  # Invalid types
        )


def test_file_refs_rejects_null_byte():
    """file_refs rejects paths containing null bytes."""
    with pytest.raises(ValidationError, match="control characters"):
        PromptRequest(cli="gemini", prompt="test", file_refs=["/etc/\x00passwd"])


def test_file_refs_rejects_newline():
    """file_refs rejects paths containing newline characters."""
    with pytest.raises(ValidationError, match="control characters"):
        PromptRequest(cli="gemini", prompt="test", file_refs=["/etc\n/passwd"])


def test_file_refs_rejects_carriage_return():
    """file_refs rejects paths containing carriage return characters."""
    with pytest.raises(ValidationError, match="control characters"):
        PromptRequest(cli="gemini", prompt="test", file_refs=["foo\rbar"])


def test_file_refs_accepts_normal_paths():
    """file_refs accepts normal absolute and relative paths."""
    req = PromptRequest(
        cli="gemini", prompt="test", file_refs=["/home/user/file.py", "relative/path"]
    )
    assert req.file_refs == ["/home/user/file.py", "relative/path"]


def test_agent_response_with_metadata_adds_keys():
    """with_metadata() returns a new response with additional metadata keys."""
    original = AgentResponse(cli="gemini", output="hello", raw_output="{}", metadata={"k": 1})
    updated = original.with_metadata(new_key="value", count=42)

    assert updated.metadata == {"k": 1, "new_key": "value", "count": 42}


def test_agent_response_with_metadata_preserves_other_fields():
    """with_metadata() preserves agent, output, and raw_output unchanged."""
    original = AgentResponse(cli="gemini", output="hello", raw_output="{}")
    updated = original.with_metadata(x=1)

    assert updated.cli == original.cli
    assert updated.output == original.output
    assert updated.raw_output == original.raw_output


def test_agent_response_with_metadata_does_not_mutate_original():
    """with_metadata() returns a new object; the original is unchanged."""
    original = AgentResponse(cli="gemini", output="hello", raw_output="{}", metadata={"k": 1})
    original.with_metadata(k=999)

    assert original.metadata == {"k": 1}


# ---------------------------------------------------------------------------
# AgentTask / AgentTaskResult / MultiPromptResponse tests
# ---------------------------------------------------------------------------


def test_agent_task_requires_agent_and_prompt():
    task = AgentTask(cli="gemini", prompt="Hello")
    assert task.cli == "gemini"
    assert task.prompt == "Hello"
    assert task.label is None
    assert task.execution_mode is None  # None = use session preference or fall back to "default"


def test_agent_task_rejects_empty_agent():
    with pytest.raises(ValidationError):
        AgentTask(cli="", prompt="Hello")


def test_agent_task_rejects_empty_prompt():
    with pytest.raises(ValidationError):
        AgentTask(cli="gemini", prompt="")


def test_agent_task_result_success():
    result = AgentTaskResult(label="gemini", output="response text")
    assert result.success is True
    assert result.output == "response text"
    assert result.error is None


def test_agent_task_result_error():
    result = AgentTaskResult(label="gemini", error="something failed")
    assert result.success is False
    assert result.error == "something failed"
    assert result.output is None


def test_agent_task_result_rejects_both_output_and_error():
    with pytest.raises(ValidationError):
        AgentTaskResult(label="gemini", output="ok", error="fail")


def test_agent_task_result_rejects_neither_output_nor_error():
    with pytest.raises(ValidationError):
        AgentTaskResult(label="gemini")


def test_multi_prompt_response_counts():
    results = [
        AgentTaskResult(label="a", output="ok"),
        AgentTaskResult(label="b", error="fail"),
        AgentTaskResult(label="c", output="ok"),
    ]
    response = MultiPromptResponse(results=results)
    assert response.total == 3
    assert response.succeeded == 2
    assert response.failed == 1


# ---------------------------------------------------------------------------
# max_retries field tests
# ---------------------------------------------------------------------------


def test_prompt_request_max_retries_defaults_to_none():
    """PromptRequest.max_retries defaults to None (falls back to env default)."""
    req = PromptRequest(cli="gemini", prompt="Hello")
    assert req.max_retries is None


def test_prompt_request_max_retries_accepts_custom_value():
    """PromptRequest.max_retries accepts a positive integer."""
    req = PromptRequest(cli="gemini", prompt="Hello", max_retries=5)
    assert req.max_retries == 5


def test_prompt_request_max_retries_accepts_one():
    """max_retries=1 disables retry (run once, no retry on failure)."""
    req = PromptRequest(cli="gemini", prompt="Hello", max_retries=1)
    assert req.max_retries == 1


def test_agent_task_max_retries_defaults_to_none():
    """AgentTask.max_retries defaults to None."""
    task = AgentTask(cli="gemini", prompt="Hello")
    assert task.max_retries is None


def test_agent_task_max_retries_accepts_custom_value():
    """AgentTask.max_retries accepts a positive integer."""
    task = AgentTask(cli="gemini", prompt="Hello", max_retries=2)
    assert task.max_retries == 2


def test_prompt_request_max_retries_zero_fails():
    """max_retries=0 is rejected (ge=1); would cause range(0) → unreachable assertion."""
    with pytest.raises(ValidationError):
        PromptRequest(cli="gemini", prompt="Hello", max_retries=0)


def test_prompt_request_max_retries_negative_fails():
    """Negative max_retries is rejected by ge=1 validator."""
    with pytest.raises(ValidationError):
        PromptRequest(cli="gemini", prompt="Hello", max_retries=-1)


def test_agent_task_max_retries_zero_fails():
    """AgentTask.max_retries=0 is rejected (ge=1)."""
    with pytest.raises(ValidationError):
        AgentTask(cli="gemini", prompt="Hello", max_retries=0)


# ---------------------------------------------------------------------------
# Prompt max_length tests (M4)
# ---------------------------------------------------------------------------


def test_prompt_request_rejects_oversized_prompt():
    """PromptRequest rejects prompts exceeding MAX_PROMPT_LENGTH (128KB)."""
    with pytest.raises(ValidationError):
        PromptRequest(cli="gemini", prompt="x" * (MAX_PROMPT_LENGTH + 1))


def test_agent_task_rejects_oversized_prompt():
    """AgentTask rejects prompts exceeding MAX_PROMPT_LENGTH (128KB)."""
    with pytest.raises(ValidationError):
        AgentTask(cli="gemini", prompt="x" * (MAX_PROMPT_LENGTH + 1))


def test_prompt_at_max_length_accepted():
    """PromptRequest accepts a prompt exactly at MAX_PROMPT_LENGTH."""
    req = PromptRequest(cli="gemini", prompt="x" * MAX_PROMPT_LENGTH)
    assert len(req.prompt) == MAX_PROMPT_LENGTH


# ---------------------------------------------------------------------------
# error_type field tests
# ---------------------------------------------------------------------------


def test_agent_task_result_with_error_type():
    """AgentTaskResult accepts error_type alongside error."""
    result = AgentTaskResult(label="t", error="bad", error_type="ParseError")
    assert result.error_type == "ParseError"
    assert result.error == "bad"


def test_agent_task_result_error_type_defaults_none():
    """AgentTaskResult.error_type defaults to None when not provided."""
    result = AgentTaskResult(label="t", output="ok")
    assert result.error_type is None


# ---------------------------------------------------------------------------
# AgentTask.to_request() tests
# ---------------------------------------------------------------------------


def test_agent_task_to_request_maps_all_fields():
    """to_request() produces a PromptRequest with all AgentTask fields copied."""
    from nexus_mcp.types import PromptRequest

    task = AgentTask(
        cli="gemini",
        prompt="Hello",
        execution_mode="yolo",
        model="gemini-2.5-flash",
        max_retries=2,
        output_limit=1024,
        timeout=30,
    )
    req = task.to_request()
    assert isinstance(req, PromptRequest)
    assert req.cli == "gemini"
    assert req.prompt == "Hello"
    assert req.execution_mode == "yolo"
    assert req.model == "gemini-2.5-flash"
    assert req.max_retries == 2
    assert req.output_limit == 1024
    assert req.timeout == 30


def test_agent_task_to_request_defaults():
    """to_request() preserves default values when optional fields are unset."""
    task = AgentTask(cli="gemini", prompt="Hi")
    req = task.to_request()
    assert req.execution_mode == "default"
    assert req.model is None
    assert req.max_retries is None
    assert req.output_limit is None
    assert req.timeout is None
    assert req.context == {}


def test_prompt_request_output_limit_defaults_to_none():
    """PromptRequest.output_limit defaults to None (falls back to env default)."""
    req = PromptRequest(cli="gemini", prompt="Hello")
    assert req.output_limit is None


def test_prompt_request_output_limit_accepts_positive():
    """PromptRequest.output_limit accepts a positive integer."""
    req = PromptRequest(cli="gemini", prompt="Hello", output_limit=4096)
    assert req.output_limit == 4096


def test_prompt_request_output_limit_rejects_zero():
    """PromptRequest.output_limit=0 is rejected by ge=1."""
    with pytest.raises(ValidationError):
        PromptRequest(cli="gemini", prompt="Hello", output_limit=0)


def test_prompt_request_timeout_defaults_to_none():
    """PromptRequest.timeout defaults to None (falls back to env default)."""
    req = PromptRequest(cli="gemini", prompt="Hello")
    assert req.timeout is None


def test_prompt_request_timeout_accepts_positive():
    """PromptRequest.timeout accepts a positive integer."""
    req = PromptRequest(cli="gemini", prompt="Hello", timeout=60)
    assert req.timeout == 60


def test_prompt_request_timeout_rejects_zero():
    """PromptRequest.timeout=0 is rejected by ge=1."""
    with pytest.raises(ValidationError):
        PromptRequest(cli="gemini", prompt="Hello", timeout=0)


def test_agent_task_output_limit_defaults_to_none():
    """AgentTask.output_limit defaults to None."""
    task = AgentTask(cli="gemini", prompt="Hello")
    assert task.output_limit is None


def test_agent_task_timeout_defaults_to_none():
    """AgentTask.timeout defaults to None."""
    task = AgentTask(cli="gemini", prompt="Hello")
    assert task.timeout is None


# ---------------------------------------------------------------------------
# AgentTaskResult.formatted_error tests
# ---------------------------------------------------------------------------


def test_agent_task_result_formatted_error_with_type():
    """formatted_error prefixes the message with [ErrorType] when error_type is set."""
    result = AgentTaskResult(label="t", error="bad input", error_type="ParseError")
    assert result.formatted_error == "[ParseError] bad input"


def test_agent_task_result_formatted_error_without_type():
    """formatted_error returns the bare message when error_type is None."""
    result = AgentTaskResult(label="t", error="something went wrong")
    assert result.formatted_error == "something went wrong"


# ---------------------------------------------------------------------------
# SessionPreferences tests
# ---------------------------------------------------------------------------


class TestSessionPreferences:
    def test_default_values_are_none(self):
        """SessionPreferences defaults: all fields are None."""
        prefs = SessionPreferences()
        assert prefs.execution_mode is None
        assert prefs.model is None
        assert prefs.max_retries is None
        assert prefs.output_limit is None
        assert prefs.timeout is None

    def test_accepts_valid_execution_modes(self):
        """All ExecutionMode values are accepted."""
        for mode in ("default", "yolo"):
            prefs = SessionPreferences(execution_mode=mode)
            assert prefs.execution_mode == mode

    def test_rejects_invalid_execution_mode(self):
        """Invalid execution mode raises ValidationError."""
        with pytest.raises(ValidationError):
            SessionPreferences(execution_mode="invalid")  # type: ignore[arg-type]

    def test_none_execution_mode_is_valid(self):
        """None is explicitly valid for execution_mode."""
        prefs = SessionPreferences(execution_mode=None)
        assert prefs.execution_mode is None

    def test_rejects_empty_string_model(self):
        """Empty string model is rejected by min_length=1."""
        with pytest.raises(ValidationError):
            SessionPreferences(model="")

    def test_none_model_is_valid(self):
        """None is explicitly valid for model."""
        prefs = SessionPreferences(model=None)
        assert prefs.model is None

    def test_model_dump_round_trip(self):
        """model_dump() → SessionPreferences(**d) round-trip preserves values."""
        prefs = SessionPreferences(
            execution_mode="yolo",
            model="gemini-2.5-flash",
            max_retries=3,
            output_limit=1024,
            timeout=30,
        )
        d = prefs.model_dump()
        reconstructed = SessionPreferences(**d)
        assert reconstructed.execution_mode == "yolo"
        assert reconstructed.model == "gemini-2.5-flash"
        assert reconstructed.max_retries == 3
        assert reconstructed.output_limit == 1024
        assert reconstructed.timeout == 30

    def test_max_retries_accepts_positive(self):
        """max_retries accepts a positive integer."""
        prefs = SessionPreferences(max_retries=5)
        assert prefs.max_retries == 5

    def test_max_retries_rejects_zero(self):
        """max_retries=0 is rejected by ge=1."""
        with pytest.raises(ValidationError):
            SessionPreferences(max_retries=0)

    def test_max_retries_rejects_negative(self):
        """Negative max_retries is rejected by ge=1."""
        with pytest.raises(ValidationError):
            SessionPreferences(max_retries=-1)

    def test_output_limit_accepts_positive(self):
        """output_limit accepts a positive integer."""
        prefs = SessionPreferences(output_limit=4096)
        assert prefs.output_limit == 4096

    def test_output_limit_rejects_zero(self):
        """output_limit=0 is rejected by ge=1."""
        with pytest.raises(ValidationError):
            SessionPreferences(output_limit=0)

    def test_timeout_accepts_positive(self):
        """timeout accepts a positive integer."""
        prefs = SessionPreferences(timeout=60)
        assert prefs.timeout == 60

    def test_timeout_rejects_zero(self):
        """timeout=0 is rejected by ge=1."""
        with pytest.raises(ValidationError):
            SessionPreferences(timeout=0)

    def test_timeout_rejects_negative(self):
        """Negative timeout is rejected by ge=1."""
        with pytest.raises(ValidationError):
            SessionPreferences(timeout=-10)

    def test_frozen(self):
        """SessionPreferences is immutable (frozen=True)."""
        prefs = SessionPreferences(execution_mode="default")
        with pytest.raises(ValidationError):
            prefs.execution_mode = "yolo"  # type: ignore[misc]


# ---------------------------------------------------------------------------
# Task 1: SessionPreferences elicitation fields
# ---------------------------------------------------------------------------


class TestSessionPreferencesElicitation:
    def test_elicit_defaults_to_none(self):
        """elicit field defaults to None."""
        prefs = SessionPreferences()
        assert prefs.elicit is None

    def test_elicit_accepts_true(self):
        """elicit field accepts True."""
        prefs = SessionPreferences(elicit=True)
        assert prefs.elicit is True

    def test_elicit_accepts_false(self):
        """elicit field accepts False."""
        prefs = SessionPreferences(elicit=False)
        assert prefs.elicit is False

    def test_confirm_yolo_defaults_to_none(self):
        """confirm_yolo field defaults to None."""
        prefs = SessionPreferences()
        assert prefs.confirm_yolo is None

    def test_confirm_yolo_accepts_bool(self):
        """confirm_yolo accepts bool values."""
        assert SessionPreferences(confirm_yolo=True).confirm_yolo is True
        assert SessionPreferences(confirm_yolo=False).confirm_yolo is False

    def test_confirm_vague_prompt_defaults_to_none(self):
        """confirm_vague_prompt field defaults to None."""
        prefs = SessionPreferences()
        assert prefs.confirm_vague_prompt is None

    def test_confirm_vague_prompt_accepts_bool(self):
        """confirm_vague_prompt accepts bool values."""
        assert SessionPreferences(confirm_vague_prompt=True).confirm_vague_prompt is True
        assert SessionPreferences(confirm_vague_prompt=False).confirm_vague_prompt is False

    def test_confirm_high_retries_defaults_to_none(self):
        """confirm_high_retries field defaults to None."""
        prefs = SessionPreferences()
        assert prefs.confirm_high_retries is None

    def test_confirm_high_retries_accepts_bool(self):
        """confirm_high_retries accepts bool values."""
        assert SessionPreferences(confirm_high_retries=True).confirm_high_retries is True
        assert SessionPreferences(confirm_high_retries=False).confirm_high_retries is False

    def test_confirm_large_batch_defaults_to_none(self):
        """confirm_large_batch field defaults to None."""
        prefs = SessionPreferences()
        assert prefs.confirm_large_batch is None

    def test_confirm_large_batch_accepts_bool(self):
        """confirm_large_batch accepts bool values."""
        assert SessionPreferences(confirm_large_batch=True).confirm_large_batch is True
        assert SessionPreferences(confirm_large_batch=False).confirm_large_batch is False

    def test_model_dump_includes_all_elicitation_fields(self):
        """model_dump() includes all 5 new elicitation fields."""
        prefs = SessionPreferences(
            elicit=True,
            confirm_yolo=False,
            confirm_vague_prompt=True,
            confirm_high_retries=False,
            confirm_large_batch=True,
        )
        d = prefs.model_dump()
        assert d["elicit"] is True
        assert d["confirm_yolo"] is False
        assert d["confirm_vague_prompt"] is True
        assert d["confirm_high_retries"] is False
        assert d["confirm_large_batch"] is True

    def test_model_dump_all_none_by_default(self):
        """model_dump() shows all elicitation fields as None by default."""
        d = SessionPreferences().model_dump()
        for field in (
            "elicit",
            "confirm_yolo",
            "confirm_vague_prompt",
            "confirm_high_retries",
            "confirm_large_batch",
        ):
            assert d[field] is None, f"{field} should be None in default dump"


# ---------------------------------------------------------------------------
# Task 2: AgentTask.cli optional
# ---------------------------------------------------------------------------


class TestAgentTaskOptionalCli:
    def test_cli_none_is_valid(self):
        """cli=None is valid (enables elicitation disambiguation)."""
        task = AgentTask(cli=None, prompt="Hello")
        assert task.cli is None

    def test_cli_defaults_to_none_when_omitted(self):
        """cli defaults to None when not provided."""
        task = AgentTask(prompt="Hello")
        assert task.cli is None

    def test_cli_empty_string_rejected(self):
        """cli='' is rejected by min_length=1."""
        with pytest.raises(ValidationError):
            AgentTask(cli="", prompt="Hello")

    def test_cli_valid_string_still_works(self):
        """cli='gemini' still works as before."""
        task = AgentTask(cli="gemini", prompt="Hello")
        assert task.cli == "gemini"

    def test_to_request_with_cli_none_raises_validation_error(self):
        """to_request() with cli=None raises ValidationError (cli required in PromptRequest)."""
        task = AgentTask(cli=None, prompt="Hello")
        with pytest.raises(ValidationError):
            task.to_request()
