package testutils

import (
	"bufio"
	"bytes"
	"fmt"
	"log"
	"os"
	"strings"
	"testing"
)

// A LogHarness runs sets of assertions against the log output of a function. Assertions are grouped
// into sequences of messages that are expected to be found in the log output. Calling one of the Expect
// methods on the harness adds an expectation to the default sequence of messages. Additional sequences
// can be created by calling NewSequence.
type LogHarness struct {
	buf       bytes.Buffer
	sequences []*Sequence
}

type Expectation interface {
	IsMatch(line string) bool
	String() string
}

// Run executes the function f and captures any output written using Go's standard log. Each sequence
// of expected messages is then
func (h *LogHarness) Run(t *testing.T, f func()) {
	// Capture raw log output
	fl := log.Flags()
	log.SetFlags(0)
	log.SetOutput(&h.buf)
	f()
	log.SetFlags(fl)
	log.SetOutput(os.Stderr)

	for _, seq := range h.sequences {
		seq.Assert(t, bufio.NewScanner(bytes.NewReader(h.buf.Bytes())))
	}
}

// Expect adds an expectation to the default sequence that the log contains a line equal to s
func (h *LogHarness) Expect(s string) {
	if len(h.sequences) == 0 {
		h.sequences = append(h.sequences, &Sequence{name: ""})
	}
	h.sequences[0].Expect(s)
}

// ExpectPrefix adds an to the default sequence expectation that the log contains a line starting with s
func (h *LogHarness) ExpectPrefix(s string) {
	if len(h.sequences) == 0 {
		h.sequences = append(h.sequences, &Sequence{name: ""})
	}
	h.sequences[0].ExpectPrefix(s)
}

// NewSequence creates a new sequence of expected log messages
func (h *LogHarness) NewSequence(name string) *Sequence {
	seq := &Sequence{name: name}
	h.sequences = append(h.sequences, seq)
	return seq
}

type prefix string

func (p prefix) IsMatch(line string) bool {
	return strings.HasPrefix(line, string(p))
}

func (p prefix) String() string {
	return fmt.Sprintf("prefix %q", string(p))
}

type text string

func (t text) IsMatch(line string) bool {
	return line == string(t)
}

func (t text) String() string {
	return fmt.Sprintf("text %q", string(t))
}

type Sequence struct {
	name string
	exp  []Expectation
}

func (seq *Sequence) Assert(t *testing.T, s *bufio.Scanner) {
	var tag string
	if seq.name != "" {
		tag = fmt.Sprintf("[%s] ", seq.name)
	}
	// Match raw log lines against expectations
exploop:
	for _, e := range seq.exp {
		for s.Scan() {
			if e.IsMatch(s.Text()) {
				t.Logf("%ssaw: %s", tag, s.Text())
				continue exploop
			}
		}
		if s.Err() == nil {
			t.Errorf("%sdid not see expected %s", tag, e.String())
			return
		}
	}
}

// Expect adds an expectation that the log contains a line equal to s
func (seq *Sequence) Expect(s string) {
	seq.exp = append(seq.exp, text(s))
}

// ExpectPrefix adds an expectation that the log contains a line starting with s
func (seq *Sequence) ExpectPrefix(s string) {
	seq.exp = append(seq.exp, prefix(s))
}