//go:build !windows
// +build !windows

package gexec_test

import (
	"io"
	"os/exec"
	"syscall"
	"time"

	. "github.com/onsi/gomega/gbytes"
	. "github.com/onsi/gomega/gexec"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
)

var _ = Describe("Session", func() {
	Context("firefly binary", func() {
		var fireflyPath string
		var command *exec.Cmd
		var session *Session

		var outWriter, errWriter io.Writer

		BeforeEach(func() {
			outWriter = nil
			errWriter = nil

			var err error
			fireflyPath, err = Build("./_fixture/firefly")
			Expect(err).ShouldNot(HaveOccurred())

		})

		JustBeforeEach(func() {
			command = exec.Command(fireflyPath)
			var err error
			session, err = Start(command, outWriter, errWriter)
			Expect(err).ShouldNot(HaveOccurred())
		})

		Context("running a command", func() {
			It("should start the process", func() {
				Expect(command.Process).ShouldNot(BeNil())
			})

			It("should wrap the process's stdout and stderr with gbytes buffers", func() {
				Eventually(session.Out).Should(Say("We've done the impossible, and that makes us mighty"))
				Eventually(session.Err).Should(Say("Ah, curse your sudden but inevitable betrayal!"))
				defer session.Out.CancelDetects()

				select {
				case <-session.Out.Detect("Can we maybe vote on the whole murdering people issue"):
					Eventually(session).Should(Exit(0))
				case <-session.Out.Detect("I swear by my pretty floral bonnet, I will end you."):
					Eventually(session).Should(Exit(1))
				case <-session.Out.Detect("My work's illegal, but at least it's honest."):
					Eventually(session).Should(Exit(2))
				case <-time.After(5 * time.Second):
					Fail("timed out waiting for detection")
				}
			})

			It("should satisfy the gbytes.BufferProvider interface, passing Stdout", func() {
				Eventually(session).Should(Say("We've done the impossible, and that makes us mighty"))
				Eventually(session).Should(Exit())
			})
		})

		Describe("providing the exit code", func() {
			It("should provide the app's exit code", func() {
				Expect(session.ExitCode()).Should(Equal(-1))

				Eventually(session).Should(Exit())
				Expect(session.ExitCode()).Should(BeNumerically(">=", 0))
				Expect(session.ExitCode()).Should(BeNumerically("<", 3))
			})
		})

		Describe("wait", func() {
			It("should wait till the command exits", func() {
				Expect(session.ExitCode()).Should(Equal(-1))
				Expect(session.Wait().ExitCode()).Should(BeNumerically(">=", 0))
				Expect(session.Wait().ExitCode()).Should(BeNumerically("<", 3))
			})
		})

		Describe("exited", func() {
			It("should close when the command exits", func() {
				Eventually(session.Exited).Should(BeClosed())
				Expect(session.ExitCode()).ShouldNot(Equal(-1))
			})
		})

		Describe("kill", func() {
			It("should kill the command", func() {
				session, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
				Expect(err).ShouldNot(HaveOccurred())

				session.Kill()
				Eventually(session).Should(Exit(128 + 9))
			})
		})

		Describe("interrupt", func() {
			It("should interrupt the command", func() {
				session, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
				Expect(err).ShouldNot(HaveOccurred())

				session.Interrupt()
				Eventually(session).Should(Exit(128 + 2))
			})
		})

		Describe("terminate", func() {
			It("should terminate the command", func() {
				session, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
				Expect(err).ShouldNot(HaveOccurred())

				session.Terminate()
				Eventually(session).Should(Exit(128 + 15))
			})
		})

		Describe("signal", func() {
			It("should send the signal to the command", func() {
				session, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
				Expect(err).ShouldNot(HaveOccurred())

				session.Signal(syscall.SIGABRT)
				Eventually(session).WithTimeout(5 * time.Second).Should(Exit(128 + 6))
			})

			It("should ignore sending a signal if the command did not start", func() {
				session, err := Start(exec.Command("notexisting"), GinkgoWriter, GinkgoWriter)
				Expect(err).To(HaveOccurred())

				Expect(func() { session.Signal(syscall.SIGUSR1) }).NotTo(Panic())
			})
		})

		Context("tracking sessions", func() {
			BeforeEach(func() {
				KillAndWait()
			})

			Describe("kill", func() {
				It("should kill all the started sessions", func() {
					session1, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
					Expect(err).ShouldNot(HaveOccurred())

					session2, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
					Expect(err).ShouldNot(HaveOccurred())

					session3, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
					Expect(err).ShouldNot(HaveOccurred())

					Kill()

					Eventually(session1).Should(Exit(128 + 9))
					Eventually(session2).Should(Exit(128 + 9))
					Eventually(session3).Should(Exit(128 + 9))
				})

				It("should not track unstarted sessions", func() {
					_, err := Start(exec.Command("does not exist", "10000000"), GinkgoWriter, GinkgoWriter)
					Expect(err).Should(HaveOccurred())

					session2, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
					Expect(err).ShouldNot(HaveOccurred())

					session3, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
					Expect(err).ShouldNot(HaveOccurred())

					Kill()

					Eventually(session2).Should(Exit(128 + 9))
					Eventually(session3).Should(Exit(128 + 9))
				})

			})

			Describe("killAndWait", func() {
				It("should kill all the started sessions and wait for them to finish", func() {
					session1, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
					Expect(err).ShouldNot(HaveOccurred())

					session2, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
					Expect(err).ShouldNot(HaveOccurred())

					session3, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
					Expect(err).ShouldNot(HaveOccurred())

					KillAndWait()
					Expect(session1).Should(Exit(128+9), "Should have exited")
					Expect(session2).Should(Exit(128+9), "Should have exited")
					Expect(session3).Should(Exit(128+9), "Should have exited")
				})
			})

			Describe("terminate", func() {
				It("should terminate all the started sessions", func() {
					session1, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
					Expect(err).ShouldNot(HaveOccurred())

					session2, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
					Expect(err).ShouldNot(HaveOccurred())

					session3, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
					Expect(err).ShouldNot(HaveOccurred())

					Terminate()

					Eventually(session1).Should(Exit(128 + 15))
					Eventually(session2).Should(Exit(128 + 15))
					Eventually(session3).Should(Exit(128 + 15))
				})
			})

			Describe("terminateAndWait", func() {
				It("should terminate all the started sessions, and wait for them to exit", func() {
					session1, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
					Expect(err).ShouldNot(HaveOccurred())

					session2, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
					Expect(err).ShouldNot(HaveOccurred())

					session3, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
					Expect(err).ShouldNot(HaveOccurred())

					TerminateAndWait()

					Expect(session1).Should(Exit(128+15), "Should have exited")
					Expect(session2).Should(Exit(128+15), "Should have exited")
					Expect(session3).Should(Exit(128+15), "Should have exited")
				})
			})

			Describe("signal", func() {
				It("should signal all the started sessions", func() {
					session1, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
					Expect(err).ShouldNot(HaveOccurred())

					session2, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
					Expect(err).ShouldNot(HaveOccurred())

					session3, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
					Expect(err).ShouldNot(HaveOccurred())

					Signal(syscall.SIGABRT)

					Eventually(session1).WithTimeout(5 * time.Second).Should(Exit(128 + 6))
					Eventually(session2).WithTimeout(5 * time.Second).Should(Exit(128 + 6))
					Eventually(session3).WithTimeout(5 * time.Second).Should(Exit(128 + 6))
				})
			})

			Describe("interrupt", func() {
				It("should interrupt all the started sessions, and not wait", func() {
					session1, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
					Expect(err).ShouldNot(HaveOccurred())

					session2, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
					Expect(err).ShouldNot(HaveOccurred())

					session3, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
					Expect(err).ShouldNot(HaveOccurred())

					Interrupt()

					Eventually(session1).Should(Exit(128 + 2))
					Eventually(session2).Should(Exit(128 + 2))
					Eventually(session3).Should(Exit(128 + 2))
				})
			})
		})

		When("the command exits", func() {
			It("should close the buffers", func() {
				Eventually(session).Should(Exit())

				Expect(session.Out.Closed()).Should(BeTrue())
				Expect(session.Err.Closed()).Should(BeTrue())

				Expect(session.Out).Should(Say("We've done the impossible, and that makes us mighty"))
			})

			var So = It

			So("this means that eventually should short circuit", func() {
				t := time.Now()
				failures := InterceptGomegaFailures(func() {
					Eventually(session).Should(Say("blah blah blah blah blah"))
				})
				Expect(time.Since(t)).Should(BeNumerically("<", time.Second))
				Expect(failures).Should(HaveLen(1))
			})
		})

		When("wrapping out and err", func() {
			var (
				outWriterBuffer, errWriterBuffer *Buffer
			)

			BeforeEach(func() {
				outWriterBuffer = NewBuffer()
				outWriter = outWriterBuffer
				errWriterBuffer = NewBuffer()
				errWriter = errWriterBuffer
			})

			It("should route to both the provided writers and the gbytes buffers", func() {
				Eventually(session.Out).Should(Say("We've done the impossible, and that makes us mighty"))
				Eventually(session.Err).Should(Say("Ah, curse your sudden but inevitable betrayal!"))

				Expect(outWriterBuffer.Contents()).Should(ContainSubstring("We've done the impossible, and that makes us mighty"))
				Expect(errWriterBuffer.Contents()).Should(ContainSubstring("Ah, curse your sudden but inevitable betrayal!"))

				Eventually(session).Should(Exit())

				Expect(outWriterBuffer.Contents()).Should(Equal(session.Out.Contents()))
				Expect(errWriterBuffer.Contents()).Should(Equal(session.Err.Contents()))
			})

			When("discarding the output of the command", func() {
				BeforeEach(func() {
					outWriter = io.Discard
					errWriter = io.Discard
				})

				It("executes successfully", func() {
					Eventually(session).Should(Exit())
				})
			})
		})
	})

	Context("firefly tests", func() {
		var fireflyTestPath string
		var command *exec.Cmd
		var session *Session

		var outWriter, errWriter io.Writer

		BeforeEach(func() {
			outWriter = nil
			errWriter = nil

			var err error
			fireflyTestPath, err = CompileTest("./_fixture/firefly")
			Expect(err).ShouldNot(HaveOccurred())
		})

		JustBeforeEach(func() {
			command = exec.Command(fireflyTestPath)
			var err error
			session, err = Start(command, outWriter, errWriter)
			Expect(err).ShouldNot(HaveOccurred())
		})

		When("wrapping out and err", func() {
			var (
				outWriterBuffer, errWriterBuffer *Buffer
			)

			BeforeEach(func() {
				outWriterBuffer = NewBuffer()
				outWriter = outWriterBuffer
				errWriterBuffer = NewBuffer()
				errWriter = errWriterBuffer
			})

			It("should route to both the provided writers and the gbytes buffers", func() {
				Eventually(session.Out).Should(Say("PASS"))
				Eventually(session.Err).Should(Say(""))

				Expect(outWriterBuffer.Contents()).Should(ContainSubstring("PASS"))
				Expect(errWriterBuffer.Contents()).Should(BeEmpty())

				Eventually(session).Should(Exit())

				Expect(outWriterBuffer.Contents()).Should(Equal(session.Out.Contents()))
				Expect(errWriterBuffer.Contents()).Should(Equal(session.Err.Contents()))
			})

			When("discarding the output of the command", func() {
				BeforeEach(func() {
					outWriter = io.Discard
					errWriter = io.Discard
				})

				It("executes successfully", func() {
					Eventually(session).Should(Exit())
				})
			})
		})
	})

	Describe("when the command fails to start", func() {
		It("should return an error", func() {
			_, err := Start(exec.Command("agklsjdfas"), nil, nil)
			Expect(err).Should(HaveOccurred())
		})
	})
})
