package subprocess

import (
	"bytes"
	"context"
	"errors"
	"os"
	"path/filepath"
	"runtime"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestRun(t *testing.T) {
	t.Run("Success", func(t *testing.T) {
		dir := t.TempDir()
		var mirror bytes.Buffer
		res := Run(Options{
			Ctx:     context.Background(),
			Command: "echo hello && echo world 1>&2",
			Cwd:     dir,
			Mirror:  &mirror,
		})
		require.NoError(t, res.Err)
		assert.Contains(t, mirror.String(), "hello")
		assert.Contains(t, mirror.String(), "world")
		assert.Contains(t, res.Output, "hello")
		assert.Contains(t, res.Output, "world")
	})

	t.Run("NonZeroExit", func(t *testing.T) {
		res := Run(Options{
			Ctx:     context.Background(),
			Command: "exit 7",
			Cwd:     t.TempDir(),
		})
		assert.Error(t, res.Err)
		var se *Error
		require.ErrorAs(t, res.Err, &se)
		assert.Equal(t, 7, se.Code)
	})

	t.Run("StartFailure", func(t *testing.T) {
		// cmd.Start fails before any subprocess output when Cwd is missing.
		missing := filepath.Join(t.TempDir(), "does-not-exist")
		res := Run(Options{
			Ctx:     context.Background(),
			Command: "true",
			Cwd:     missing,
		})
		assert.Error(t, res.Err)
		var se *Error
		assert.False(t, errors.As(res.Err, &se), "Err = %v, want non-Error (infra)", res.Err)
	})

	t.Run("MirrorReceivesOutput", func(t *testing.T) {
		var mirror bytes.Buffer
		res := Run(Options{
			Ctx:     context.Background(),
			Command: "printf 'a\\nb\\nc\\n' && printf 'd\\ne\\n' 1>&2",
			Cwd:     t.TempDir(),
			Mirror:  &mirror,
		})
		require.NoError(t, res.Err)
		got := mirror.String()
		for _, want := range []string{"a", "b", "c", "d", "e"} {
			assert.Contains(t, got, want+"\n")
		}
	})

	t.Run("NilMirrorOk", func(t *testing.T) {
		res := Run(Options{
			Ctx:     context.Background(),
			Command: "echo hi",
			Cwd:     t.TempDir(),
			Mirror:  nil,
		})
		require.NoError(t, res.Err)
		assert.Equal(t, "hi\n", res.Output)
	})

	t.Run("ContextCancel", func(t *testing.T) {
		if runtime.GOOS == "windows" {
			t.Skip("shell tests are POSIX-only")
		}
		ctx, cancel := context.WithCancel(context.Background())
		go func() {
			// Run has no process-start callback, so cancellation must wait until
			// the shell has replaced itself with the long-running command.
			time.Sleep(100 * time.Millisecond)
			cancel()
		}()
		start := time.Now()
		res := Run(Options{
			Ctx:     ctx,
			Command: "sleep 30",
			Cwd:     os.TempDir(),
		})
		elapsed := time.Since(start)
		assert.Error(t, res.Err)
		assert.LessOrEqual(t, elapsed, 5*time.Second)
	})
}

func TestErrorMessage(t *testing.T) {
	assert.Equal(t, "subprocess exited with code 7", (&Error{Code: 7}).Error())
}

func TestTailBuffer(t *testing.T) {
	t.Run("UnderCapKeepsAll", func(t *testing.T) {
		tb := newTailBuffer(8)
		_, _ = tb.Write([]byte("abc"))
		assert.Equal(t, "abc", tb.String())
	})
	t.Run("IncrementalWrapKeepsTail", func(t *testing.T) {
		tb := newTailBuffer(3)
		for _, s := range []string{"a", "b", "c", "d", "e"} {
			_, _ = tb.Write([]byte(s))
		}
		assert.Equal(t, "cde", tb.String())
	})
	t.Run("SingleWriteOverCapKeepsTail", func(t *testing.T) {
		tb := newTailBuffer(4)
		_, _ = tb.Write([]byte("abcdefgh"))
		assert.Equal(t, "efgh", tb.String())
	})
	t.Run("WriteExactlyCap", func(t *testing.T) {
		tb := newTailBuffer(4)
		_, _ = tb.Write([]byte("wxyz"))
		assert.Equal(t, "wxyz", tb.String())
	})
	t.Run("WrapThenOverwrite", func(t *testing.T) {
		tb := newTailBuffer(3)
		_, _ = tb.Write([]byte("abc"))
		_, _ = tb.Write([]byte("de"))
		assert.Equal(t, "cde", tb.String())
	})
	t.Run("EmptyWriteIsNoop", func(t *testing.T) {
		tb := newTailBuffer(4)
		_, _ = tb.Write([]byte("ab"))
		n, err := tb.Write(nil)
		require.NoError(t, err)
		assert.Zero(t, n)
		assert.Equal(t, "ab", tb.String())
	})
	t.Run("ZeroCapCoercedToOne", func(t *testing.T) {
		tb := newTailBuffer(0)
		assert.Empty(t, tb.String())
		_, _ = tb.Write([]byte("ab"))
		assert.Equal(t, "b", tb.String())
	})
}
