package cli_test

import (
	"context"
	"io"
	"strings"
	"sync"
	"testing"
	"time"

	featurebase "github.com/featurebasedb/featurebase/v3"
	"github.com/featurebasedb/featurebase/v3/cli"
	"github.com/featurebasedb/featurebase/v3/logger"
	"github.com/pkg/errors"
	"github.com/stretchr/testify/assert"
)

func TestCLI(t *testing.T) {
	t.Run("Input", func(t *testing.T) {
		ctx := context.Background()

		capture := newCapture(t)

		cli := cli.NewCLICommand(logger.StderrLogger)
		cli.Stdin = capture
		cli.Stdout = capture
		cli.Queryer = capture

		go func() {
			assert.NoError(t, cli.Run(ctx))
		}()

		none := []string{}

		// One statement, one line.
		capture.Assert("one;", []string{`one`})

		// One statement, multiple lines.
		capture.Assert("one", none)
		capture.Assert(" two ", none)
		capture.Assert("three;", []string{`one
 two 
three`})

		// Multiple statements, one line.
		capture.Assert("foo; bar;", []string{`foo`, `bar`})

		// Multiple statements, multiple lines.
		capture.Assert("a1", none)
		capture.Assert("a2; b1", []string{`a1
a2`})
		capture.Assert("b2;", []string{`b1
b2`})

		// Blank lines.
		capture.Assert("one", none)
		capture.Assert("", none)
		capture.Assert("three;", []string{`one

three`})

		// Just a semi-colon.
		capture.Assert(";", none)

		// Multi-line with just a semi-colon.
		capture.Assert("one", none)
		capture.Assert(";", []string{`one`})

		// Ensure a clean exit with no errors.
		assert.NoError(t, capture.Exit())
	})
}

////////////////////////////////////////////////////////

// Ensure type implementes interface.
var _ io.ReadCloser = (*capture)(nil)
var _ io.Writer = (*capture)(nil)
var _ cli.Queryer = (*capture)(nil)

// capture implements the various CLI interfaces in order to capture test input
// and submit it as though that input were being read from the command line. It
// also captures calls made to the Queryer.Query method and ensures the sql the
// contain is expected.
type capture struct {
	t *testing.T

	// ch is a channel of strings (one line at a time) of CLI input.
	ch chan string

	mu   sync.RWMutex
	sqls []string

	// queryDone will receive an event any time the Query method is called and
	// has completed. This is to tell the Assert method that it's safe to
	// compare the sqls slice.
	queryDone chan struct{}

	asserting chan struct{}

	err error
}

func newCapture(t *testing.T) *capture {
	return &capture{
		t:         t,
		ch:        make(chan string),
		sqls:      make([]string, 0),
		queryDone: make(chan struct{}),
	}
}

func (c *capture) Exit() error {
	c.sendLine("exit")
	c.mu.RLock()
	defer c.mu.RUnlock()
	return c.err
}

func (c *capture) Assert(in string, out []string) {
	c.asserting = make(chan struct{})

	c.sendLine(in)

	// Wait for the CLI command to complete processing the input and send the
	// sql to Query() by blocking on the queryDone channel. Because Query gets
	// called for every sql statement in the input, an input resulting in
	// multiple sql statements needs to wait for all expected queries to
	// complete. A timeout is included to this so it doesn't deadlock in the
	// case where Query is expected to be called, but isn't; after the timeout,
	// the test should fail completely. In summary: we wait on queryDone the
	// number of sql statements we expect. If we receive fewer than expected,
	// the timeout will occur. If we receive more than expected, the Query()
	// method will effectively deadlock, reach its own timout, then write to
	// capture.err, which will be reported upon Exit().
	for range out {
		select {
		case <-c.queryDone:
		case <-time.After(2 * time.Second):
			c.t.Fatalf("expected Query() to be called")
		}
	}

	close(c.asserting)

	c.mu.Lock()
	defer c.mu.Unlock()

	assert.Equal(c.t, out, c.sqls)

	// Reset the slice.
	c.sqls = c.sqls[:0]

}

// sendLine sends the given string as a line input to the CLI command. It
// appends a line feed to the end of string in order to mimic the user hitting
// the return key.
func (c *capture) sendLine(s string) {
	// Add a line feed before putting s on the channel in order to mimic the
	// user hitting the return key.
	c.ch <- s + "\n"
}

// Read is read by the CLI in place of user input. It effectively sends lines of
// input to the CLI, getting each line to be sent off the channel.
func (c *capture) Read(b []byte) (n int, err error) {
	s := <-c.ch
	return strings.NewReader(s).Read(b)
}

func (c *capture) Close() error {
	close(c.ch)
	return nil
}

// Write is called with anything written to output. This would included results
// from calling Query() under normal, non-testing conditions, as well as other
// informational text sent to output, such as the splash message.
func (c *capture) Write(b []byte) (n int, err error) {
	return 0, nil
}

// Query is called by the CLI command once a full SQL statement is received
// (signified by the terminator: `;`).
func (c *capture) Query(org, db, sql string) (*featurebase.WireQueryResponse, error) {
	c.mu.Lock()
	c.sqls = append(c.sqls, sql)
	c.mu.Unlock()

	select {
	case c.queryDone <- struct{}{}:
	case <-c.asserting:
		c.mu.Lock()
		c.err = errors.Errorf("unexpected query: %s", sql)
		c.mu.Unlock()
	}
	return &featurebase.WireQueryResponse{}, nil
}
