import os
import subprocess
import tempfile

import pytest

from soniclue_basekit.cli_formatter import CliFormatter
from soniclue_basekit.script_runner import ScriptRunner, ScriptRunnerError, _TIMEOUT_RETURN_CODE


# --- ScriptRunnerError ---


def test_error_is_timeout_true():
    e = ScriptRunnerError(cmd=[], returncode=_TIMEOUT_RETURN_CODE, stdout=None, stderr=None)
    assert e.is_timeout is True


def test_error_is_timeout_false():
    e = ScriptRunnerError(cmd=[], returncode=1, stdout=None, stderr=None)
    assert e.is_timeout is False


def test_error_str_includes_returncode():
    e = ScriptRunnerError(cmd=[], returncode=2, stdout=None, stderr=None)
    assert "returned 2" in str(e)


def test_error_str_timeout():
    e = ScriptRunnerError(cmd=[], returncode=_TIMEOUT_RETURN_CODE, stdout=None, stderr=None)
    assert "timed out" in str(e)


def test_error_str_includes_stdout():
    e = ScriptRunnerError(cmd=[], returncode=1, stdout="out text", stderr=None)
    assert "out text" in str(e)


def test_error_str_includes_stderr():
    e = ScriptRunnerError(cmd=[], returncode=1, stdout=None, stderr="err text")
    assert "err text" in str(e)


def test_error_str_omits_none_stdout():
    e = ScriptRunnerError(cmd=[], returncode=1, stdout=None, stderr=None)
    assert "stdout" not in str(e)


# --- ScriptRunner.as_temp_file ---


def test_as_temp_file_creates_and_deletes():
    runner = ScriptRunner("echo hi")
    with runner.as_temp_file(None, prefix="test_") as path:
        assert os.path.exists(path)
    assert not os.path.exists(path)


def test_as_temp_file_writes_content():
    content = "#!/bin/sh\necho hello"
    runner = ScriptRunner(content)
    with runner.as_temp_file(None) as path:
        with open(path) as f:
            assert f.read() == content


def test_as_temp_file_is_path_yields_directly(tmp_path):
    existing = tmp_path / "existing.sh"
    existing.write_text("echo hi")
    runner = ScriptRunner(str(existing), is_path=True)
    with runner.as_temp_file(None) as path:
        assert path == str(existing)
    assert existing.exists()  # Not deleted.


# --- ScriptRunner.run ---


def test_run_executes_script():
    runner = ScriptRunner("#!/bin/sh\nexit 0")
    runner.run()  # Should not raise.


def test_run_raises_on_nonzero_exit():
    runner = ScriptRunner("#!/bin/sh\nexit 42")
    with pytest.raises(ScriptRunnerError) as exc_info:
        runner.run()
    assert exc_info.value.returncode == 42


def test_run_with_output_captures_stdout():
    runner = ScriptRunner("#!/bin/sh\necho hello world")
    stdout, stderr = runner.run(with_output=True)
    assert stdout == "hello world"
    assert stderr == ""


def test_run_with_output_captures_stderr():
    runner = ScriptRunner("#!/bin/sh\necho err >&2")
    stdout, stderr = runner.run(with_output=True)
    assert stdout == ""
    assert stderr == "err"


def test_run_with_output_strips_whitespace():
    runner = ScriptRunner("#!/bin/sh\nprintf '  spaces  '")
    stdout, _ = runner.run(with_output=True)
    assert stdout == "spaces"


def test_run_with_fmt_passes_args():
    # An empty-string value emits the key as a bare positional token.
    runner = ScriptRunner("#!/bin/sh\necho $1")
    fmt = CliFormatter()
    fmt.add("hello", "")
    stdout, _ = runner.run(fmt=fmt, with_output=True)
    assert stdout == "hello"


def test_run_log_file_redirects_output(tmp_path):
    log = tmp_path / "out.log"
    with open(log, "w") as fh:
        runner = ScriptRunner("#!/bin/sh\necho logged")
        runner.run(log_file_handle=fh)
    assert "logged" in log.read_text()


def test_run_with_output_and_log_file_raises():
    runner = ScriptRunner("#!/bin/sh\necho hi")
    with pytest.raises(AssertionError):
        runner.run(with_output=True, log_file_handle=open(os.devnull, "w"))


def test_run_with_output_and_custom_runner_raises():
    runner = ScriptRunner("#!/bin/sh\necho hi")
    with pytest.raises(AssertionError):
        runner.run(with_output=True, cmd_runner=lambda a, b, c: None)


def test_run_custom_cmd_runner():
    calls = []

    def my_runner(cmd_array, work_dir, env):
        calls.append(cmd_array)

    runner = ScriptRunner("#!/bin/sh\necho hi")
    runner.run(cmd_runner=my_runner)
    assert len(calls) == 1


def test_run_env_passes_variables():
    runner = ScriptRunner("#!/bin/sh\necho $MY_VAR")
    stdout, _ = runner.run(with_output=True, env={**os.environ, "MY_VAR": "soniclue"})
    assert stdout == "soniclue"


def test_run_work_dir(tmp_path):
    runner = ScriptRunner("#!/bin/sh\npwd")
    stdout, _ = runner.run(with_output=True, work_dir=str(tmp_path))
    assert stdout.rstrip("/") == str(tmp_path).rstrip("/")


def test_run_is_path_with_existing_script(tmp_path):
    script = tmp_path / "hello.sh"
    script.write_text("#!/bin/sh\necho from_file")
    script.chmod(script.stat().st_mode | 0o111)
    runner = ScriptRunner(str(script), is_path=True)
    stdout, _ = runner.run(with_output=True)
    assert stdout == "from_file"


def test_run_timeout_kills_long_running_script():
    runner = ScriptRunner("#!/bin/sh\nsleep 60")
    with pytest.raises(ScriptRunnerError) as exc_info:
        runner.run(timeout_sec=0.5)
    assert exc_info.value.is_timeout


def test_run_timeout_kill_after():
    # Script catches SIGINT and keeps running; --kill-after forces SIGKILL.
    runner = ScriptRunner("#!/bin/sh\ntrap '' INT\nsleep 60")
    with pytest.raises(ScriptRunnerError) as exc_info:
        runner.run(timeout_sec=0.5, timeout_kill_after_sec=1)
    assert exc_info.value.returncode != 0


# --- ScriptRunner.run_with_output ---


def test_run_with_output_shortcut():
    runner = ScriptRunner("#!/bin/sh\necho shortcut")
    stdout, stderr = runner.run_with_output()
    assert stdout == "shortcut"
    assert stderr == ""
