"""Tests for ``arborist.qa.progress`` — stage-level state-machine
emitter for ``arborist query``.

Module was zero-coverage. 85 LOC of pure stdlib emitter +
env/TTY/cli-override resolution. Surface:

  - Progress.emit (formatting, key=value tail, stream redirect)
  - disabled() singleton (true no-op, no formatting cost)
  - from_env(cli_override=...) precedence resolution
  - ARBORIST_PROGRESS env var (multiple truthy/falsy spellings)
  - TTY auto-detect fallback
"""

from __future__ import annotations

import io
import time

import pytest

from arborist.qa.progress import Progress, disabled, from_env


# --- Progress.emit --------------------------------------------------


def test_progress_emit_writes_to_stream():
    buf = io.StringIO()
    p = Progress(enabled=True, t_start=time.monotonic(), stream=buf)
    p.emit("stage.name", k="v")
    out = buf.getvalue()
    assert "stage.name" in out
    assert "k=v" in out
    assert "arborist" in out  # prefix


def test_progress_emit_disabled_writes_nothing():
    """`enabled=False` is a true no-op — no string formatting,
    no stream write."""
    buf = io.StringIO()
    p = Progress(enabled=False, t_start=time.monotonic(), stream=buf)
    p.emit("stage", a=1)
    assert buf.getvalue() == ""


def test_progress_emit_no_kv_writes_bare_prefix():
    """emit('stage') with no kwargs → just the timestamp + stage."""
    buf = io.StringIO()
    p = Progress(enabled=True, t_start=time.monotonic(), stream=buf)
    p.emit("stage.bare")
    out = buf.getvalue()
    assert "stage.bare" in out
    assert "=" not in out  # no key=value tail


def test_progress_emit_includes_elapsed_seconds():
    """Elapsed is wall-clock since t_start. Format `NN.NNs`."""
    t0 = time.monotonic() - 1.5  # pretend started 1.5s ago
    buf = io.StringIO()
    p = Progress(enabled=True, t_start=t0, stream=buf)
    p.emit("now")
    out = buf.getvalue()
    # Format includes "1.5" or higher; exact value drifts but the
    # decimal format is fixed at 6.2f.
    assert "s]" in out  # closing bracket on elapsed
    # Token before "s]" should parse as a positive float.
    head = out.split("s]")[0]
    elapsed_str = head.split()[-1]
    assert float(elapsed_str) >= 1.5


def test_progress_emit_multiple_kv_pairs_space_separated():
    buf = io.StringIO()
    p = Progress(enabled=True, t_start=time.monotonic(), stream=buf)
    p.emit("multi", a=1, b="two", c=3.14)
    out = buf.getvalue()
    assert "a=1" in out
    assert "b=two" in out
    assert "c=3.14" in out


def test_progress_emit_default_stream_is_stderr(capsys):
    """When stream kwarg is None, default is sys.stderr."""
    p = Progress(enabled=True, t_start=time.monotonic(), stream=None)
    p.emit("default-stream")
    captured = capsys.readouterr()
    assert captured.out == ""
    assert "default-stream" in captured.err


def test_progress_emit_default_t_start_is_now():
    """If t_start kwarg is None, defaults to monotonic() at
    construction. Elapsed at first emit() should be ~0."""
    buf = io.StringIO()
    p = Progress(enabled=True, t_start=None, stream=buf)
    p.emit("right-after-init")
    out = buf.getvalue()
    elapsed_str = out.split("s]")[0].split()[-1]
    assert float(elapsed_str) < 0.1  # virtually no time elapsed


# --- disabled() singleton -------------------------------------------


def test_disabled_returns_singleton():
    """disabled() returns the same Progress instance on every call.
    Suitable as a default kwarg value with no allocation."""
    a = disabled()
    b = disabled()
    assert a is b
    assert a.enabled is False


def test_disabled_emits_nothing():
    p = disabled()
    # disabled() singleton has no test-injectable stream; emit must
    # still no-op without raising.
    p.emit("stage", k="v")
    # No assertion — just confirming no exception path.


# --- from_env precedence --------------------------------------------


def test_from_env_cli_override_on(monkeypatch):
    """cli_override='on' wins over env + TTY."""
    monkeypatch.setenv("ARBORIST_PROGRESS", "0")
    p = from_env(cli_override="on")
    assert p.enabled is True


def test_from_env_cli_override_off(monkeypatch):
    """cli_override='off' wins over env."""
    monkeypatch.setenv("ARBORIST_PROGRESS", "1")
    p = from_env(cli_override="off")
    assert p.enabled is False


@pytest.mark.parametrize("env_value", ["1", "true", "on", "yes",
                                        "TRUE", "On", "YES"])
def test_from_env_truthy_env_enables(monkeypatch, env_value):
    """ARBORIST_PROGRESS in {1, true, on, yes} (case-insensitive)
    → enabled=True."""
    monkeypatch.setenv("ARBORIST_PROGRESS", env_value)
    p = from_env()
    assert p.enabled is True


@pytest.mark.parametrize("env_value", ["0", "false", "off", "no",
                                        "FALSE", "Off", "NO"])
def test_from_env_falsy_env_disables(monkeypatch, env_value):
    monkeypatch.setenv("ARBORIST_PROGRESS", env_value)
    p = from_env()
    assert p.enabled is False


def test_from_env_unknown_env_falls_through_to_tty(monkeypatch):
    """ARBORIST_PROGRESS=garbage → fall through to TTY auto-detect."""
    monkeypatch.setenv("ARBORIST_PROGRESS", "maybe")
    # Force stderr to be non-TTY (we're in a test, so usually it is).
    class _NonTTY:
        def isatty(self):
            return False
    monkeypatch.setattr("arborist.qa.progress.sys.stderr", _NonTTY())
    p = from_env()
    assert p.enabled is False


def test_from_env_unset_env_falls_through_to_tty(monkeypatch):
    """No ARBORIST_PROGRESS in env → TTY detect."""
    monkeypatch.delenv("ARBORIST_PROGRESS", raising=False)
    class _IsATTY:
        def isatty(self):
            return True
    monkeypatch.setattr("arborist.qa.progress.sys.stderr", _IsATTY())
    p = from_env()
    assert p.enabled is True


def test_from_env_isatty_attribute_error_disables(monkeypatch):
    """If sys.stderr lacks .isatty (replaced with a buffer), the
    fallback is enabled=False (fail-closed)."""
    monkeypatch.delenv("ARBORIST_PROGRESS", raising=False)

    class _NoIsATTY:
        pass

    monkeypatch.setattr("arborist.qa.progress.sys.stderr", _NoIsATTY())
    p = from_env()
    assert p.enabled is False


def test_from_env_returns_fresh_instance(monkeypatch):
    """Each from_env() call returns a new Progress with a fresh
    t_start; never shares state with prior calls."""
    monkeypatch.setenv("ARBORIST_PROGRESS", "1")
    a = from_env()
    time.sleep(0.001)
    b = from_env()
    assert a is not b
    assert b.t_start >= a.t_start


# --- empty-string env -----------------------------------------------


def test_from_env_empty_string_falls_through(monkeypatch):
    """ARBORIST_PROGRESS='' (empty) → fall through to TTY."""
    monkeypatch.setenv("ARBORIST_PROGRESS", "")
    class _NonTTY:
        def isatty(self):
            return False
    monkeypatch.setattr("arborist.qa.progress.sys.stderr", _NonTTY())
    p = from_env()
    assert p.enabled is False


def test_from_env_whitespace_env_falls_through(monkeypatch):
    """ARBORIST_PROGRESS='  ' (whitespace) → strip → empty → TTY."""
    monkeypatch.setenv("ARBORIST_PROGRESS", "   ")
    class _IsATTY:
        def isatty(self):
            return True
    monkeypatch.setattr("arborist.qa.progress.sys.stderr", _IsATTY())
    p = from_env()
    assert p.enabled is True
