"""CLI smoke tests."""

from unittest.mock import patch

import pytest
from click.testing import CliRunner

from md2mip.cli import cli
from tests.conftest import knapsack_fixture_with_data, load_fixture


class TestCompileCommand:
    def test_compile_ir_only(self, tmp_path):
        """Test --ir-only flag outputs to stdout."""
        fixture = load_fixture("transportation")
        model_file = tmp_path / "model.md"
        model_file.write_text("# Test model")

        with patch("md2mip.compiler.parse_model", return_value=fixture):
            runner = CliRunner()
            result = runner.invoke(
                cli,
                [
                    "compile",
                    str(model_file),
                    "--ir-only",
                ],
            )
            assert result.exit_code == 0
            assert '"transportation"' in result.output

    def test_compile_default_writes_to_out_dir(self, tmp_path):
        """Without -o, compile writes to out/<stem>_solver.py."""
        fixture = load_fixture("transportation")
        model_file = tmp_path / "mymodel.md"
        model_file.write_text("# Test model")
        out_dir = tmp_path / "out"

        with (
            patch("md2mip.compiler.parse_model", return_value=fixture),
            patch("md2mip.cli.OUT_DIR", out_dir),
        ):
            runner = CliRunner()
            result = runner.invoke(cli, ["compile", str(model_file)])

        assert result.exit_code == 0
        out_file = out_dir / "mymodel_solver.py"
        assert out_file.exists()
        assert "import highspy" in out_file.read_text()
        assert "Written:" in result.output
        assert "Run:" in result.output

    def test_compile_to_file(self, tmp_path):
        """Test -o flag writes to explicit path."""
        fixture = load_fixture("transportation")
        model_file = tmp_path / "model.md"
        model_file.write_text("# Test model")
        out_file = tmp_path / "solver.py"

        with patch("md2mip.compiler.parse_model", return_value=fixture):
            runner = CliRunner()
            result = runner.invoke(
                cli,
                [
                    "compile",
                    str(model_file),
                    "-o",
                    str(out_file),
                ],
            )
            assert result.exit_code == 0
            assert out_file.exists()
            content = out_file.read_text()
            assert "import highspy" in content

    def test_compile_passes_model_flag(self, tmp_path):
        """Test --model flag is forwarded to LLM."""
        fixture = load_fixture("transportation")
        model_file = tmp_path / "model.md"
        model_file.write_text("# Test model")

        with patch("md2mip.compiler.parse_model", return_value=fixture) as mock:
            runner = CliRunner()
            result = runner.invoke(
                cli,
                [
                    "compile",
                    str(model_file),
                    "--ir-only",
                    "--model",
                    "ollama/qwen3",
                ],
            )
            assert result.exit_code == 0
            mock.assert_called_once()
            assert mock.call_args[1]["model"] == "ollama/qwen3"

    def test_compile_confidence_report_no_warnings(self, tmp_path):
        """Confidence report shows parsed counts and 'high' when no warnings."""
        fixture = load_fixture("transportation")
        model_file = tmp_path / "model.md"
        model_file.write_text("# Test model")

        with patch("md2mip.compiler.parse_model", return_value=fixture):
            runner = CliRunner()
            result = runner.invoke(cli, ["compile", str(model_file), "--ir-only"])
            assert result.exit_code == 0
            assert "Parsed: 2 sets, 3 params, 1 vars, 2 constraints" in result.output
            assert "Confidence: high (no warnings)" in result.output

    def test_compile_confidence_report_with_warnings(self, tmp_path):
        """Confidence report surfaces LLM warnings."""
        fixture = load_fixture("transportation")
        fixture["warnings"] = ["Assumed non-negativity for x", "Ambiguous indexing"]
        model_file = tmp_path / "model.md"
        model_file.write_text("# Test model")

        with patch("md2mip.compiler.parse_model", return_value=fixture):
            runner = CliRunner()
            result = runner.invoke(cli, ["compile", str(model_file), "--ir-only"])
            assert result.exit_code == 0
            assert "WARNING: Assumed non-negativity for x" in result.output
            assert "WARNING: Ambiguous indexing" in result.output
            assert "Confidence: high" not in result.output

    def test_compile_llm_error(self, tmp_path):
        """Test graceful error when LLM fails."""
        from md2mip.llm import LLMError

        model_file = tmp_path / "model.md"
        model_file.write_text("# Test model")

        with patch("md2mip.compiler.parse_model", side_effect=LLMError("API down")):
            runner = CliRunner()
            result = runner.invoke(
                cli,
                [
                    "compile",
                    str(model_file),
                    "--ir-only",
                ],
            )
            assert result.exit_code != 0


class TestOcrCommand:
    def test_ocr_to_stdout(self, tmp_path):
        """Mock ocr_image, check stdout."""
        img_file = tmp_path / "model.png"
        img_file.write_bytes(b"\x89PNG" + b"\x00" * 100)

        with patch("md2mip.cli.ocr_image", return_value="# Extracted Model") as mock:
            runner = CliRunner()
            result = runner.invoke(cli, ["ocr", str(img_file)])
            assert result.exit_code == 0
            assert "Extracted Model" in result.output
            mock.assert_called_once()

    def test_ocr_to_file(self, tmp_path):
        """Mock ocr_image, check file written."""
        img_file = tmp_path / "model.png"
        img_file.write_bytes(b"\x89PNG" + b"\x00" * 100)
        out_file = tmp_path / "model.md"

        with patch("md2mip.cli.ocr_image", return_value="# Extracted Model"):
            runner = CliRunner()
            result = runner.invoke(cli, ["ocr", str(img_file), "-o", str(out_file)])
            assert result.exit_code == 0
            assert out_file.exists()
            assert "Extracted Model" in out_file.read_text()

    def test_ocr_passes_model_flag(self, tmp_path):
        """Verify --model forwarded."""
        img_file = tmp_path / "model.png"
        img_file.write_bytes(b"\x89PNG" + b"\x00" * 100)

        with patch("md2mip.cli.ocr_image", return_value="# Model") as mock:
            runner = CliRunner()
            result = runner.invoke(
                cli,
                [
                    "ocr",
                    str(img_file),
                    "--model",
                    "gpt-4o",
                ],
            )
            assert result.exit_code == 0
            mock.assert_called_once_with(str(img_file), model="gpt-4o")


class TestValidateCommand:
    def test_validate_pass(self, tmp_path):
        """Mock LLM, run validate with correct expected objective, exit 0."""
        fixture = load_fixture("transportation")
        model_file = tmp_path / "model.md"
        model_file.write_text("# Test model")

        # validate_model calls compile_to_python which calls parse_model
        with patch("md2mip.compiler.parse_model", return_value=fixture):
            from tests.conftest import DATA_DIR

            data_path = DATA_DIR / "transportation.yaml"
            runner = CliRunner()
            result = runner.invoke(
                cli,
                [
                    "validate",
                    str(model_file),
                    "--data",
                    str(data_path),
                    "--expect-objective",
                    "215",
                ],
            )
            assert result.exit_code == 0, f"Output: {result.output}"
            assert "PASS" in result.output

    def test_validate_fail(self, tmp_path):
        """Mock LLM, run validate with wrong expected objective, exit 1."""
        fixture = load_fixture("transportation")
        model_file = tmp_path / "model.md"
        model_file.write_text("# Test model")

        with patch("md2mip.compiler.parse_model", return_value=fixture):
            from tests.conftest import DATA_DIR

            data_path = DATA_DIR / "transportation.yaml"
            runner = CliRunner()
            result = runner.invoke(
                cli,
                [
                    "validate",
                    str(model_file),
                    "--data",
                    str(data_path),
                    "--expect-objective",
                    "999",
                ],
            )
            assert result.exit_code == 1
            assert "FAIL" in result.output


class TestCompileDataTemplate:
    def test_compile_writes_data_template(self, tmp_path):
        """Compile produces a data YAML template alongside the solver."""
        fixture = load_fixture("transportation")
        model_file = tmp_path / "mymodel.md"
        model_file.write_text("# Test model")
        out_dir = tmp_path / "out"

        with (
            patch("md2mip.compiler.parse_model", return_value=fixture),
            patch("md2mip.cli.OUT_DIR", out_dir),
        ):
            runner = CliRunner()
            result = runner.invoke(cli, ["compile", str(model_file)])

        assert result.exit_code == 0
        data_file = out_dir / "mymodel_data.yaml"
        assert data_file.exists()
        content = data_file.read_text()
        assert "Data template" in content
        assert "I:" in content
        assert "mymodel_data.yaml" in result.output

    def test_compile_writes_complete_data_when_ir_has_data(self, tmp_path):
        """When IR has inline data, the data file is complete."""
        fixture = knapsack_fixture_with_data()
        model_file = tmp_path / "knapsack.md"
        model_file.write_text("# Knapsack")
        out_dir = tmp_path / "out"

        with (
            patch("md2mip.compiler.parse_model", return_value=fixture),
            patch("md2mip.cli.OUT_DIR", out_dir),
        ):
            runner = CliRunner()
            result = runner.invoke(cli, ["compile", str(model_file)])

        assert result.exit_code == 0
        data_file = out_dir / "knapsack_data.yaml"
        assert data_file.exists()
        content = data_file.read_text()
        assert "Data for knapsack" in content
        assert "item1" in content


class TestRunCommand:
    def test_run_without_data_no_inline_errors(self, tmp_path):
        """Run without --data and no inline data gives clear error."""
        fixture = load_fixture("transportation")
        model_file = tmp_path / "model.md"
        model_file.write_text("# Test model")

        with patch("md2mip.compiler.parse_model", return_value=fixture):
            runner = CliRunner()
            result = runner.invoke(cli, ["run", str(model_file)])
            assert result.exit_code != 0

    @pytest.mark.solver
    def test_run_with_inline_data_succeeds(self, tmp_path):
        """Run without --data succeeds when IR has inline data (README example 1)."""
        fixture = knapsack_fixture_with_data()
        model_file = tmp_path / "knapsack.md"
        model_file.write_text("# Knapsack")

        with patch("md2mip.compiler.parse_model", return_value=fixture):
            runner = CliRunner()
            result = runner.invoke(cli, ["run", str(model_file)])
            assert result.exit_code == 0, f"Output: {result.output}"

    def test_run_requires_data(self):
        runner = CliRunner()
        result = runner.invoke(cli, ["run", "nonexistent.md"])
        assert result.exit_code != 0


class TestReadmeExamples:
    """Tests that verify the README examples produce the documented output."""

    @pytest.mark.solver
    def test_readme_example1_knapsack_run(self, tmp_path, capfd):
        """README example 1: run knapsack with inline data."""
        fixture = knapsack_fixture_with_data()
        model_file = tmp_path / "knapsack.md"
        model_file.write_text("# Knapsack")

        with patch("md2mip.compiler.parse_model", return_value=fixture):
            runner = CliRunner()
            result = runner.invoke(cli, ["run", str(model_file)])
            assert result.exit_code == 0, f"Output: {result.output}"

        captured = capfd.readouterr()
        assert "Objective: 15.0" in captured.out
        assert "x[item2] = 1.0" in captured.out

    def test_readme_example2_compile_output(self, tmp_path):
        """README example 2: compile transportation shows Written + Run lines."""
        fixture = load_fixture("transportation")
        model_file = tmp_path / "transportation.md"
        model_file.write_text("# Transportation")
        out_dir = tmp_path / "out"

        with (
            patch("md2mip.compiler.parse_model", return_value=fixture),
            patch("md2mip.cli.OUT_DIR", out_dir),
        ):
            runner = CliRunner()
            result = runner.invoke(cli, ["compile", str(model_file)])

        assert result.exit_code == 0
        assert "Written:" in result.output
        assert "transportation_solver.py" in result.output
        assert "transportation_data.yaml" in result.output
        assert (out_dir / "transportation_solver.py").exists()
        assert (out_dir / "transportation_data.yaml").exists()

    @pytest.mark.solver
    def test_readme_example2_run_with_data(self, tmp_path, capfd):
        """README example 2: run transportation with data file."""
        from tests.conftest import DATA_DIR

        fixture = load_fixture("transportation")
        model_file = tmp_path / "transportation.md"
        model_file.write_text("# Transportation")
        data_path = DATA_DIR / "transportation.yaml"

        with patch("md2mip.compiler.parse_model", return_value=fixture):
            runner = CliRunner()
            result = runner.invoke(cli, ["run", str(model_file), "--data", str(data_path)])
            assert result.exit_code == 0, f"Output: {result.output}"

        captured = capfd.readouterr()
        assert "Objective: 215.0" in captured.out

    def test_readme_example3_ocr_output(self, tmp_path):
        """README example 3: OCR writes file and shows extraction message."""
        img_file = tmp_path / "knapsack_photo.png"
        img_file.write_bytes(b"\x89PNG" + b"\x00" * 100)
        out_file = tmp_path / "model.md"

        with patch("md2mip.cli.ocr_image", return_value="# Extracted Model"):
            runner = CliRunner()
            result = runner.invoke(cli, ["ocr", str(img_file), "-o", str(out_file)])
            assert result.exit_code == 0
            assert "Extracted model from" in result.output
            assert "Written:" in result.output
            assert "Run:     md2mip compile" in result.output
            assert out_file.exists()
