// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package monitor

import (
	"encoding/json"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/hashicorp/consul/agent"
	"github.com/mitchellh/cli"
)

func TestMonitorCommand_exitsOnSignalBeforeLinesArrive(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()
	a := agent.StartTestAgent(t, agent.TestAgent{})
	defer a.Shutdown()

	shutdownCh := make(chan struct{})

	ui := cli.NewMockUi()
	c := New(ui, shutdownCh)
	// Only wait for ERR which shouldn't happen so should leave logs empty for a
	// while
	args := []string{"-http-addr=" + a.HTTPAddr(), "-log-level=ERR"}

	// Buffer it so we don't deadlock when blocking send on shutdownCh triggers
	// Run to return before we can select on it.
	exitCode := make(chan int, 1)

	// Run the monitor in another go routine. If this doesn't exit on our "signal"
	// then the whole test will hang and we'll panic (to not blow up if people run
	// the suite without -timeout)
	var wg sync.WaitGroup
	wg.Add(1)
	go func() {
		wg.Done() // Signal that this goroutine is at least running now
		exitCode <- c.Run(args)
	}()

	// Wait for that routine to at least be running
	wg.Wait()

	// Simulate signal in a few milliseconds without blocking this thread
	go func() {
		time.Sleep(5 * time.Millisecond)
		shutdownCh <- struct{}{}
	}()

	// Wait for a second - shouldn't take long to handle the signal before we
	// panic. We simulate inside the select since the other goroutine might
	// already have exited if there was some error and we'd block forever trying
	// to write to unbuffered shutdownCh. We don't just buffer it because then it
	// doesn't model the real shutdownCh we use for signal watching and could mask
	// bugs in the handling.
	select {
	case ret := <-exitCode:
		if ret != 0 {
			t.Fatal("command returned with non-zero code")
		}
		// OK!
	case <-time.After(1 * time.Second):
		t.Fatal("timed out waiting for exit")
	}
}

func TestMonitorCommand_LogJSONValidFlag(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()
	a := agent.StartTestAgent(t, agent.TestAgent{})
	defer a.Shutdown()

	shutdownCh := make(chan struct{})

	ui := cli.NewMockUi()
	c := New(ui, shutdownCh)
	args := []string{"-http-addr=" + a.HTTPAddr(), "-log-json"}

	// Buffer it so we don't deadlock when blocking send on shutdownCh triggers
	// Run to return before we can select on it.
	exitCode := make(chan int, 1)

	// Run the monitor in another go routine. If this doesn't exit on our "signal"
	// then the whole test will hang and we'll panic (to not blow up if people run
	// the suite without -timeout)
	var wg sync.WaitGroup
	wg.Add(1)
	go func() {
		wg.Done() // Signal that this goroutine is at least running now
		exitCode <- c.Run(args)
	}()

	// Wait for that routine to at least be running
	wg.Wait()

	// Simulate signal in a few milliseconds without blocking this thread
	go func() {
		time.Sleep(5 * time.Millisecond)
		shutdownCh <- struct{}{}
	}()

	// Wait for a second - shouldn't take long to handle the signal before we
	// panic. We simulate inside the select since the other goroutine might
	// already have exited if there was some error and we'd block forever trying
	// to write to unbuffered shutdownCh. We don't just buffer it because then it
	// doesn't model the real shutdownCh we use for signal watching and could mask
	// bugs in the handling.
	select {
	case ret := <-exitCode:
		if ret != 0 {
			t.Fatal("command returned with non-zero code")
		}
		// OK!
	case <-time.After(1 * time.Second):
		t.Fatal("timed out waiting for exit")
	}
}

func TestMonitorCommand_LogJSONValidFormat(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()
	a := agent.StartTestAgent(t, agent.TestAgent{})
	defer a.Shutdown()

	shutdownCh := make(chan struct{})

	ui := cli.NewMockUi()
	c := New(ui, shutdownCh)
	args := []string{"-http-addr=" + a.HTTPAddr(), "-log-json"}

	// Buffer it so we don't deadlock when blocking send on shutdownCh triggers
	// Run to return before we can select on it.
	exitCode := make(chan int, 1)

	// Run the monitor in another go routine. If this doesn't exit on our "signal"
	// then the whole test will hang and we'll panic (to not blow up if people run
	// the suite without -timeout)
	var wg sync.WaitGroup
	wg.Add(1)
	go func() {
		wg.Done() // Signal that this goroutine is at least running now
		exitCode <- c.Run(args)
	}()

	// Wait for that routine to at least be running
	wg.Wait()

	// Read the logs and try to json marshall it
	go func() {
		time.Sleep(1 * time.Second)
		outputs := ui.OutputWriter.String()
		for count, output := range strings.Split(outputs, "\n") {
			if output != "" && count > 0 {
				jsonLog := new(map[string]interface{})
				err := json.Unmarshal([]byte(output), jsonLog)
				if err != nil {
					exitCode <- -1
				}
				if len(*jsonLog) <= 0 {
					exitCode <- 1
				}
			}
		}
		shutdownCh <- struct{}{}

	}()

	select {
	case ret := <-exitCode:
		if ret != 0 {
			t.Fatal("command returned with non-zero code")
		}
		// OK!
	case <-time.After(5 * time.Second):
		t.Fatal("timed out waiting for exit")
	}
}
