#!/usr/bin/env bash

# An interactive terminal gets the live install region: a header with the
# install-wide bar, one row per active tool, and a final summary naming changed
# versions. Successful rows disappear. This drives mise through a PTY with CI
# unset so the animated display is actually chosen.
export CI=0 MISE_DEBUG=0 MISE_TRACE=0 MISE_LOG_LEVEL=info
export MISE_FORCE_PROGRESS=0

cat >mise.toml <<'TOML'
[tools]
dummy = { version = "1.0.0", postinstall = "sleep 1; echo hook-finished" }
TOML

python3 - <<'PY'
import errno
import fcntl
import os
import pty
import re
import struct
import subprocess
import termios

def run(args, env, columns=120):
    master, slave = pty.openpty()
    # A fresh PTY has no size; mise reads the window size, not $COLUMNS.
    fcntl.ioctl(slave, termios.TIOCSWINSZ, struct.pack("HHHH", 40, columns, 0, 0))
    proc = subprocess.Popen(args, stdout=subprocess.DEVNULL, stderr=slave, env=env)
    os.close(slave)
    output = bytearray()
    try:
        while True:
            try:
                chunk = os.read(master, 8192)
            except OSError as error:
                if error.errno == errno.EIO:
                    break
                raise
            if not chunk:
                break
            output.extend(chunk)
    finally:
        os.close(master)
    return proc.wait(), output.decode(errors="replace")

env = dict(os.environ, COLUMNS="120", LINES="40")
env.pop("CI", None)
env["CI"] = "0"
code, raw = run(["mise", "install"], env)
assert code == 0, raw
plain = re.sub(r"\x1b\[[0-9;?]*[a-zA-Z]", "", raw)

# The live region was drawn: cursor movement or line erasure appears (colors
# alone would not prove a redraw), and the header carries the install-wide bar
# with its whole-tool count.
assert re.search(r"\x1b\[[0-9;]*[ABCDEFGHJKST]|\x1b\[\?25[lh]", raw), raw
assert "░" in plain or "█" in plain, plain
assert re.search(r"[01]/1", plain), plain
assert "by @jdx" in plain, plain

# The row named the phase. (A hook's stdout is a transient status line in the
# interactive display, as it always was; the append-only reporter is what
# prints it per line.)
assert "running postinstall hook" in plain, plain

# The bar never reads full while the tool is still working: with one tool the
# header shows at most 23 of 24 cells until the completion line lands.
for line in plain.splitlines():
    if "0/1" in line and "█" * 24 in line:
        raise AssertionError("bar was full before the install finished:\n" + plain)

# The summary retains the installed version without a per-tool completion line.
assert not re.search(r"✓ dummy@1\.0\.0", plain), plain
assert re.search(r"installed 1 tool in .*: dummy@1\.0\.0", plain), plain
assert "installed 1 tool in" in plain, plain
assert "[0/1]" not in plain and "[1/1]" not in plain, plain

# Already-installed checks leave no completion rows or progress summary.
code, raw = run(["mise", "install", "dummy@1.0.0"], env)
assert code == 0, raw
plain = re.sub(r"\x1b\[[0-9;?]*[a-zA-Z]", "", raw)
assert "already installed" not in plain, plain
assert "installed 0 tools" not in plain, plain

# A narrow terminal gets a layout that fits it rather than wrapped lines: the
# header drops its version and shrinks the bar, rows drop their bar, and the
# permanent lines stay within the width. Every line of the recorded output is
# checked, so a live-region frame that overflowed would fail too.
code, raw = run(["mise", "install", "--force", "dummy@1.0.0"], env, columns=50)
assert code == 0, raw
plain = re.sub(r"\x1b\[[0-9;?]*[a-zA-Z]", "", raw)
assert re.search(r"installed 1 tool in .*: dummy@1\.0\.0", plain), plain
for line in plain.replace("\r", "\n").splitlines():
    assert len(line.rstrip()) <= 50, f"{len(line)} cells at 50 columns: {line!r}\n{plain}"
assert "█" * 11 not in plain, plain
# The byline is the last thing the header gives up, and at 50 columns it goes.
assert "by @jdx" not in plain, plain

# Implicit installation leaves its version summary before the command runs.
code, raw = run(["mise", "exec", "dummy@1.0.1", "--", "true"], env)
assert code == 0, raw
plain = re.sub(r"\x1b\[[0-9;?]*[a-zA-Z]", "", raw)
assert re.search(r"installed 1 tool in .*: dummy@1\.0\.1", plain), plain
assert "✓ dummy@1.0.1" not in plain, plain

# Failed hooks retain diagnostics and a failure summary, never success rows.
with open("mise.toml", "w") as config:
    config.write('[tools]\ndummy = { version = "1.0.2", postinstall = "echo hook-failed >&2; exit 1" }\n')
code, raw = run(["mise", "install"], env)
assert code != 0, raw
plain = re.sub(r"\x1b\[[0-9;?]*[a-zA-Z]", "", raw)
assert "✗ dummy@1.0.2" in plain, plain
assert "hook-failed" in plain, plain
assert "1 failed" in plain, plain
assert "✓ dummy@1.0.2" not in plain, plain
with open("mise.toml", "w") as config:
    config.write('[tools]\ndummy = "1.0.0"\n')

# Removal keeps the version that was removed, with no duplicate completion row.
code, raw = run(["mise", "uninstall", "dummy@1.0.1"], env)
assert code == 0, raw
plain = re.sub(r"\x1b\[[0-9;?]*[a-zA-Z]", "", raw)
assert re.search(r"removed 1 tool in .*: dummy@1\.0\.1", plain), plain
assert "✓ dummy@1.0.1" not in plain, plain

# An AI agent's terminal is a PTY that records frames instead of redrawing
# them. It must get the append-only output CI gets, or every tick reprints
# the whole live region.
agent_env = dict(env, CLAUDECODE="1")
code, raw = run(["mise", "install", "--force", "dummy@1.0.0"], agent_env)
assert code == 0, raw
assert not re.search(r"\x1b\[[0-9;]*[ABCDEFGHJKST]|\x1b\[\?25[lh]", raw), raw
plain = re.sub(r"\x1b\[[0-9;?]*[a-zA-Z]", "", raw)
assert "installed 1 tool in" in plain, plain

# --verbose keeps the classic per-line reporter.
code, raw = run(["mise", "install", "--force", "--verbose"], env)
assert code == 0, raw
plain = re.sub(r"\x1b\[[0-9;?]*[a-zA-Z]", "", raw)
assert "installed 1 tool in" not in plain, plain
PY
