"""What a user is told when an MCP server dies on them.

The Slack case, captured live: the server exits with a JSON log line carrying a Go goroutine dump,
and the app rendered the whole thing. The user sees "MCP stdio process exited unexpectedly: at
TracingChannel.traceSync (node:diagnostics_channel:322:14) { status: 1, ... }" and has no way to
learn the actual cause, which was simply that their Slack sign-in had expired.

The opposite failure matters too: inventing a friendly reason for something we do not recognise
sends people to fix the wrong thing, so anything unknown keeps its real words.
"""
from backend.apps.tools_lib.mcp_failure_reason import readable_mcp_failure

# Byte-for-byte what slack-mcp-server printed when run with the stored tokens on 2026-08-02.
SLACK_REAL = (
    '{"level":"fatal","timestamp":"2026-08-02T21:16:01-07:00","message":'
    '"Authentication failed - check your Slack tokens","app":"slack-mcp-server",'
    '"error":"invalid_auth","stacktrace":"github.com/korotovsky/slack-mcp-server/pkg/'
    'provider.newWithXOXC\\n\\t/Users/runner/work/slack-mcp-server/pkg/provider/api.go:761\\n'
    'runtime.main\\n\\t/Users/runner/hostedtoolcache/go/1.25.9/arm64/src/runtime/proc.go:285"}'
)

SLACK_NO_TOKENS = (
    '{"level":"fatal","message":"Authentication required: Either SLACK_MCP_XOXP_TOKEN, '
    'SLACK_MCP_XOXB_TOKEN, or both SLACK_MCP_XOXC_TOKEN and SLACK_MCP_XOXD_TOKEN must be provided",'
    '"app":"slack-mcp-server","stacktrace":"provider.New\\n\\tapi.go:682"}'
)

# Byte-for-byte the FULL stderr of a tokenless `npx -y slack-mcp-server` run (2026-08-04): the Go
# fatal comes FIRST, then the npm wrapper's execFileSync crash dump buries it under ~20 Node lines.
# This is the exact toast Haik and Eric saw; a translator fed only the last few lines can never win.
SLACK_NO_TOKENS_WITH_NPX_DUMP = SLACK_NO_TOKENS + """
node:child_process:963
    throw err;
    ^

Error: Command failed: /Users/x/.npm/_npx/2f12aed4e6049c73/node_modules/slack-mcp-server-darwin-arm64/bin/slack-mcp-server-darwin-arm64 --transport stdio
    at genericNodeError (node:internal/errors:983:15)
    at wrappedFn (node:internal/errors:537:14)
    at checkExecSyncError (node:child_process:924:11)
    at Object.execFileSync (node:child_process:960:15)
    at Object.<anonymous> (/Users/x/.npm/_npx/2f12aed4e6049c73/node_modules/slack-mcp-server/bin/index.js:64:14)
    at Module._compile (node:internal/modules/cjs/loader:1692:14)
    at TracingChannel.traceSync (node:diagnostics_channel:322:14) {
  status: 1,
  signal: null,
  output: [ null, null, null ],
  pid: 96234,
  stdout: null,
  stderr: null
}

Node.js v24.4.0"""


def test_the_real_slack_failure_becomes_reconnect_advice():
    out = readable_mcp_failure(SLACK_REAL)
    assert "sign-in has expired" in out
    assert "Reconnect" in out


def test_no_stacktrace_survives_into_the_message():
    out = readable_mcp_failure(SLACK_REAL)
    for leak in ("goroutine", "github.com", ".go:", "runtime.main", "stacktrace", "{"):
        assert leak not in out, f"{leak!r} leaked into what the user reads"


def test_missing_tokens_reads_differently_from_expired_ones():
    # Never signed in and signed-in-but-stale need different actions, so they cannot share a string.
    never = readable_mcp_failure(SLACK_NO_TOKENS)
    expired = readable_mcp_failure(SLACK_REAL)
    assert "signed in" in never
    assert never != expired


def test_the_npx_crash_dump_never_buries_the_real_reason():
    # The regression that survived 1.7.2: the useful fatal line is FIRST and the Node noise last.
    out = readable_mcp_failure(SLACK_NO_TOKENS_WITH_NPX_DUMP)
    assert "signed in" in out
    for leak in ("TracingChannel", "execFileSync", "node:child_process", "status: 1", "Node.js v"):
        assert leak not in out, f"{leak!r} leaked into what the user reads"


def test_revoked_access_says_so():
    assert "revoked" in readable_mcp_failure('{"error":"token_revoked","message":"bad"}').lower()


def test_a_missing_permission_points_at_reconnecting():
    out = readable_mcp_failure('{"error":"missing_scope","message":"needs channels:read"}')
    assert "permission" in out and "Reconnect" in out


def test_a_missing_binary_is_named_plainly():
    out = readable_mcp_failure("spawn npx ENOENT")
    assert "could not be found" in out


def test_rate_limiting_tells_you_to_wait_not_to_reconnect():
    out = readable_mcp_failure("Error: rate limited, retry after 30s")
    assert "rate-limiting" in out
    assert "Reconnect" not in out


def test_an_unknown_json_failure_keeps_its_real_message():
    # We must not invent a cause. Surface the server's own sentence, minus the scaffolding.
    out = readable_mcp_failure('{"level":"fatal","message":"database is locked","stacktrace":"x.go:1"}')
    assert out == "database is locked"


def test_an_unknown_plain_failure_is_passed_through():
    assert readable_mcp_failure("Segmentation fault (core dumped)") == "Segmentation fault (core dumped)"


def test_silence_is_reported_as_silence():
    out = readable_mcp_failure("")
    assert "said nothing" in out


def test_a_novel_string_is_never_guessed_at():
    out = readable_mcp_failure("could not bind to port 8080")
    assert "sign-in" not in out and "Reconnect" not in out
    assert "port 8080" in out


def test_the_last_line_wins_because_the_fatal_one_comes_last():
    noisy = '{"level":"info","message":"starting up"}\n{"level":"fatal","message":"disk full"}'
    assert readable_mcp_failure(noisy) == "disk full"


def test_output_stays_short_enough_for_a_toast():
    assert len(readable_mcp_failure("x" * 5000)) <= 300
