package internal

import (
	"bytes"
	"io"
	"os"
	"time"
)

const BAILOUT_TIME = 1 * time.Second
const BAILOUT_MESSAGE = `Ginkgo detected an issue while intercepting output.

When running in parallel, Ginkgo captures stdout and stderr output
and attaches it to the running spec.  It looks like that process is getting
stuck for this suite.

This usually happens if you, or a library you are using, spin up an external
process and set cmd.Stdout = os.Stdout and/or cmd.Stderr = os.Stderr.  This
causes the external process to keep Ginkgo's output interceptor pipe open and
causes output interception to hang.

Ginkgo has detected this and shortcircuited the capture process.  The specs
will continue running after this message however output from the external
process that caused this issue will not be captured.

You have several options to fix this.  In preferred order they are:

1. Pass GinkgoWriter instead of os.Stdout or os.Stderr to your process.
2. Ensure your process exits before the current spec completes.  If your
process is long-lived and must cross spec boundaries, this option won't
work for you.
3. Pause Ginkgo's output interceptor before starting your process and then
resume it after.  Use PauseOutputInterception() and ResumeOutputInterception()
to do this.
4. Set --output-interceptor-mode=none when running your Ginkgo suite.  This will
turn off all output interception but allow specs to run in parallel without this
issue.  You may miss important output if you do this including output from Go's
race detector.

More details on issue #851 - https://github.com/onsi/ginkgo/issues/851
`

/*
The OutputInterceptor is used by to
intercept and capture all stdin and stderr output during a test run.
*/
type OutputInterceptor interface {
	StartInterceptingOutput()
	StartInterceptingOutputAndForwardTo(io.Writer)
	StopInterceptingAndReturnOutput() string

	PauseIntercepting()
	ResumeIntercepting()

	Shutdown()
}

type NoopOutputInterceptor struct{}

func (interceptor NoopOutputInterceptor) StartInterceptingOutput()                      {}
func (interceptor NoopOutputInterceptor) StartInterceptingOutputAndForwardTo(io.Writer) {}
func (interceptor NoopOutputInterceptor) StopInterceptingAndReturnOutput() string       { return "" }
func (interceptor NoopOutputInterceptor) PauseIntercepting()                            {}
func (interceptor NoopOutputInterceptor) ResumeIntercepting()                           {}
func (interceptor NoopOutputInterceptor) Shutdown()                                     {}

type pipePair struct {
	reader *os.File
	writer *os.File
}

func startPipeFactory(pipeChannel chan pipePair, shutdown chan any) {
	for {
		//make the next pipe...
		pair := pipePair{}
		pair.reader, pair.writer, _ = os.Pipe()
		select {
		//...and provide it to the next consumer (they are responsible for closing the files)
		case pipeChannel <- pair:
			continue
		//...or close the files if we were told to shutdown
		case <-shutdown:
			pair.reader.Close()
			pair.writer.Close()
			return
		}
	}
}

type interceptorImplementation interface {
	CreateStdoutStderrClones() (*os.File, *os.File)
	ConnectPipeToStdoutStderr(*os.File)
	RestoreStdoutStderrFromClones(*os.File, *os.File)
	ShutdownClones(*os.File, *os.File)
}

type genericOutputInterceptor struct {
	intercepting bool

	stdoutClone *os.File
	stderrClone *os.File
	pipe        pipePair

	shutdown           chan any
	emergencyBailout   chan any
	pipeChannel        chan pipePair
	interceptedContent chan string

	forwardTo         io.Writer
	accumulatedOutput string

	implementation interceptorImplementation
}

func (interceptor *genericOutputInterceptor) StartInterceptingOutput() {
	interceptor.StartInterceptingOutputAndForwardTo(io.Discard)
}

func (interceptor *genericOutputInterceptor) StartInterceptingOutputAndForwardTo(w io.Writer) {
	if interceptor.intercepting {
		return
	}
	interceptor.accumulatedOutput = ""
	interceptor.forwardTo = w
	interceptor.ResumeIntercepting()
}

func (interceptor *genericOutputInterceptor) StopInterceptingAndReturnOutput() string {
	if interceptor.intercepting {
		interceptor.PauseIntercepting()
	}
	return interceptor.accumulatedOutput
}

func (interceptor *genericOutputInterceptor) ResumeIntercepting() {
	if interceptor.intercepting {
		return
	}
	interceptor.intercepting = true
	if interceptor.stdoutClone == nil {
		interceptor.stdoutClone, interceptor.stderrClone = interceptor.implementation.CreateStdoutStderrClones()
		interceptor.shutdown = make(chan any)
		go startPipeFactory(interceptor.pipeChannel, interceptor.shutdown)
	}

	// Now we make a pipe, we'll use this to redirect the input to the 1 and 2 file descriptors (this is how everything else in the world is string to log to stdout and stderr)
	// we get the pipe from our pipe factory.  it runs in the background so we can request the next pipe while the spec being intercepted is running
	interceptor.pipe = <-interceptor.pipeChannel

	interceptor.emergencyBailout = make(chan any)

	//Spin up a goroutine to copy data from the pipe into a buffer, this is how we capture any output the user is emitting
	go func() {
		buffer := &bytes.Buffer{}
		destination := io.MultiWriter(buffer, interceptor.forwardTo)
		copyFinished := make(chan any)
		reader := interceptor.pipe.reader
		go func() {
			io.Copy(destination, reader)
			reader.Close() // close the read end of the pipe so we don't leak a file descriptor
			close(copyFinished)
		}()
		select {
		case <-copyFinished:
			interceptor.interceptedContent <- buffer.String()
		case <-interceptor.emergencyBailout:
			interceptor.interceptedContent <- ""
		}
	}()

	interceptor.implementation.ConnectPipeToStdoutStderr(interceptor.pipe.writer)
}

func (interceptor *genericOutputInterceptor) PauseIntercepting() {
	if !interceptor.intercepting {
		return
	}
	// first we have to close the write end of the pipe.  To do this we have to close all file descriptors pointing
	// to the write end.  So that would be the pipewriter itself, and FD #1 and FD #2 if we've Dup2'd them
	interceptor.pipe.writer.Close() // the pipewriter itself

	// we also need to stop intercepting. we do that by reconnecting the stdout and stderr file descriptions back to their respective #1 and #2 file descriptors;
	// this also closes #1 and #2 before it points that their original stdout and stderr file descriptions
	interceptor.implementation.RestoreStdoutStderrFromClones(interceptor.stdoutClone, interceptor.stderrClone)

	var content string
	select {
	case content = <-interceptor.interceptedContent:
	case <-time.After(BAILOUT_TIME):
		/*
			By closing all the pipe writer's file descriptors associated with the pipe writer's file description the io.Copy reading from the reader
			should eventually receive an EOF and exit.

			**However**, if the user has spun up an external process and passed in os.Stdout/os.Stderr to cmd.Stdout/cmd.Stderr then the external process
			will have a file descriptor pointing to the pipe writer's file description and it will not close until the external process exits.

			That would leave us hanging here waiting for the io.Copy to close forever.  Instead we invoke this emergency escape valve.  This returns whatever
			content we've got but leaves the io.Copy running.  This ensures the external process can continue writing without hanging at the cost of leaking a goroutine
			and file descriptor (those these will be cleaned up when the process exits).

			We tack on a message to notify the user that they've hit this edgecase and encourage them to address it.
		*/
		close(interceptor.emergencyBailout)
		content = <-interceptor.interceptedContent + BAILOUT_MESSAGE
	}

	interceptor.accumulatedOutput += content
	interceptor.intercepting = false
}

func (interceptor *genericOutputInterceptor) Shutdown() {
	interceptor.PauseIntercepting()

	if interceptor.stdoutClone != nil {
		close(interceptor.shutdown)
		interceptor.implementation.ShutdownClones(interceptor.stdoutClone, interceptor.stderrClone)
		interceptor.stdoutClone = nil
		interceptor.stderrClone = nil
	}
}

/* This is used on windows builds but included here so it can be explicitly tested on unix systems too */
func NewOSGlobalReassigningOutputInterceptor() OutputInterceptor {
	return &genericOutputInterceptor{
		interceptedContent: make(chan string),
		pipeChannel:        make(chan pipePair),
		shutdown:           make(chan any),
		implementation:     &osGlobalReassigningOutputInterceptorImpl{},
	}
}

type osGlobalReassigningOutputInterceptorImpl struct{}

func (impl *osGlobalReassigningOutputInterceptorImpl) CreateStdoutStderrClones() (*os.File, *os.File) {
	return os.Stdout, os.Stderr
}

func (impl *osGlobalReassigningOutputInterceptorImpl) ConnectPipeToStdoutStderr(pipeWriter *os.File) {
	os.Stdout = pipeWriter
	os.Stderr = pipeWriter
}

func (impl *osGlobalReassigningOutputInterceptorImpl) RestoreStdoutStderrFromClones(stdoutClone *os.File, stderrClone *os.File) {
	os.Stdout = stdoutClone
	os.Stderr = stderrClone
}

func (impl *osGlobalReassigningOutputInterceptorImpl) ShutdownClones(_ *os.File, _ *os.File) {
	//noop
}
