package ui

import (
	"errors"
	"fmt"
	"testing"

	"github.com/safedep/pmg/internal/shim"
	"github.com/stretchr/testify/assert"
)

// fakeChildExit satisfies the transparentExit interface, standing in for
// *runner.ChildExitError without a cross-package import.
type fakeChildExit struct {
	code     int
	signaled bool
	pmName   string
	scrubbed int
}

func (e *fakeChildExit) Error() string {
	return fmt.Sprintf("%s exited with code %d", e.pmName, e.code)
}
func (e *fakeChildExit) ExitCode() int         { return e.code }
func (e *fakeChildExit) Transparent() bool     { return true }
func (e *fakeChildExit) IsSignaled() bool      { return e.signaled }
func (e *fakeChildExit) ScrubbedEnvCount() int { return e.scrubbed }

func withVerbosity(t *testing.T, level VerbosityLevel) {
	t.Helper()
	prev := verbosityLevel
	verbosityLevel = level
	t.Cleanup(func() { verbosityLevel = prev })
}

func TestClassifyExit(t *testing.T) {
	t.Run("genuine non-zero child exit prints a dim notice and mirrors the code", func(t *testing.T) {
		withVerbosity(t, VerbosityLevelNormal)

		d := classifyExit(&fakeChildExit{code: 1, pmName: "npm"})

		assert.True(t, d.transparent)
		assert.Equal(t, 1, d.code)
		assert.True(t, d.notice)
		assert.Equal(t, "↳ pmg: npm exited with code 1", d.message)
	})

	t.Run("transparent exit is detected even when wrapped", func(t *testing.T) {
		withVerbosity(t, VerbosityLevelNormal)

		wrapped := fmt.Errorf("failed to execute command: %w", &fakeChildExit{code: 2, pmName: "pnpm"})
		d := classifyExit(wrapped)

		assert.True(t, d.transparent)
		assert.Equal(t, 2, d.code)
		assert.True(t, d.notice)
	})

	t.Run("scrubbed env vars append a dim hint line", func(t *testing.T) {
		withVerbosity(t, VerbosityLevelNormal)

		d := classifyExit(&fakeChildExit{code: 1, pmName: "npm", scrubbed: 3})

		assert.True(t, d.notice)
		assert.Contains(t, d.message, "↳ pmg: npm exited with code 1\n")
		assert.Contains(t, d.message, "sandbox scrubbed 3 env var(s)")
		assert.Contains(t, d.message, "--sandbox-allow env=NAME")
	})

	t.Run("zero scrubbed env vars keep the notice to one line", func(t *testing.T) {
		withVerbosity(t, VerbosityLevelNormal)

		d := classifyExit(&fakeChildExit{code: 1, pmName: "npm"})

		assert.NotContains(t, d.message, "scrubbed")
		assert.NotContains(t, d.message, "\n")
	})

	t.Run("scrubbed hint is suppressed on signal exits", func(t *testing.T) {
		withVerbosity(t, VerbosityLevelNormal)

		d := classifyExit(&fakeChildExit{code: 130, signaled: true, pmName: "npm", scrubbed: 3})

		assert.False(t, d.notice)
		assert.Empty(t, d.message)
	})

	t.Run("scrubbed hint is suppressed in silent mode", func(t *testing.T) {
		withVerbosity(t, VerbosityLevelSilent)

		d := classifyExit(&fakeChildExit{code: 1, pmName: "npm", scrubbed: 3})

		assert.False(t, d.notice)
		assert.Empty(t, d.message)
	})

	t.Run("signal termination is silent but still mirrors the code", func(t *testing.T) {
		withVerbosity(t, VerbosityLevelNormal)

		d := classifyExit(&fakeChildExit{code: 130, signaled: true, pmName: "npm"})

		assert.True(t, d.transparent)
		assert.Equal(t, 130, d.code)
		assert.False(t, d.notice)
		assert.Empty(t, d.message)
	})

	t.Run("silent mode suppresses the notice", func(t *testing.T) {
		withVerbosity(t, VerbosityLevelSilent)

		d := classifyExit(&fakeChildExit{code: 1, pmName: "npm"})

		assert.True(t, d.transparent)
		assert.Equal(t, 1, d.code)
		assert.False(t, d.notice)
	})

	t.Run("non-transparent error falls through to the loud path", func(t *testing.T) {
		d := classifyExit(errors.New("some PMG failure"))

		assert.False(t, d.transparent)
		assert.False(t, d.notice)
	})
}

func TestExitFromCommandErrorExitCode(t *testing.T) {
	t.Run("missing package manager uses exit code 127", func(t *testing.T) {
		err := &shim.BinaryNotFoundError{Name: "bun"}

		var ec exitCoder
		assert.True(t, errors.As(err, &ec))
		assert.Equal(t, 127, ec.ExitCode())
	})

	t.Run("wrapped missing package manager preserves exit code 127", func(t *testing.T) {
		err := fmt.Errorf("execute: %w", &shim.BinaryNotFoundError{Name: "npm"})

		var ec exitCoder
		assert.True(t, errors.As(err, &ec))
		assert.Equal(t, 127, ec.ExitCode())
	})
}
