package tailfile // import "github.com/docker/docker/pkg/tailfile"

import (
	"bufio"
	"bytes"
	"context"
	"fmt"
	"io"
	"os"
	"strings"
	"testing"

	"gotest.tools/v3/assert"
)

func TestTailFile(t *testing.T) {
	f, err := os.CreateTemp("", "tail-test")
	if err != nil {
		t.Fatal(err)
	}
	defer f.Close()
	defer os.RemoveAll(f.Name())
	testFile := []byte(`first line
second line
third line
fourth line
fifth line
next first line
next second line
next third line
next fourth line
next fifth line
last first line
next first line
next second line
next third line
next fourth line
next fifth line
next first line
next second line
next third line
next fourth line
next fifth line
last second line
last third line
last fourth line
last fifth line
truncated line`)
	if _, err := f.Write(testFile); err != nil {
		t.Fatal(err)
	}
	if _, err := f.Seek(0, io.SeekStart); err != nil {
		t.Fatal(err)
	}
	expected := []string{"last fourth line", "last fifth line"}
	res, err := TailFile(f, 2)
	if err != nil {
		t.Fatal(err)
	}
	if len(res) != len(expected) {
		t.Fatalf("\nexpected:\n%s\n\nactual:\n%s", expected, res)
	}
	for i, l := range res {
		if expected[i] != string(l) {
			t.Fatalf("Expected line %q, got %q", expected[i], l)
		}
	}
}

func TestTailFileManyLines(t *testing.T) {
	f, err := os.CreateTemp("", "tail-test")
	if err != nil {
		t.Fatal(err)
	}
	defer f.Close()
	defer os.RemoveAll(f.Name())
	testFile := []byte(`first line
second line
truncated line`)
	if _, err := f.Write(testFile); err != nil {
		t.Fatal(err)
	}
	if _, err := f.Seek(0, io.SeekStart); err != nil {
		t.Fatal(err)
	}
	expected := []string{"first line", "second line"}
	res, err := TailFile(f, 10000)
	if err != nil {
		t.Fatal(err)
	}
	if len(expected) != len(res) {
		t.Fatalf("\nexpected:\n%s\n\nactual:\n%s", expected, res)
	}
	for i, l := range res {
		if expected[i] != string(l) {
			t.Fatalf("Expected line %s, got %s", expected[i], l)
		}
	}
}

func TestTailEmptyFile(t *testing.T) {
	f, err := os.CreateTemp("", "tail-test")
	if err != nil {
		t.Fatal(err)
	}
	defer f.Close()
	defer os.RemoveAll(f.Name())
	res, err := TailFile(f, 10000)
	if err != nil {
		t.Fatal(err)
	}
	if len(res) != 0 {
		t.Fatal("Must be empty slice from empty file")
	}
}

func TestTailNegativeN(t *testing.T) {
	f, err := os.CreateTemp("", "tail-test")
	if err != nil {
		t.Fatal(err)
	}
	defer f.Close()
	defer os.RemoveAll(f.Name())
	testFile := []byte(`first line
second line
truncated line`)
	if _, err := f.Write(testFile); err != nil {
		t.Fatal(err)
	}
	if _, err := f.Seek(0, io.SeekStart); err != nil {
		t.Fatal(err)
	}
	if _, err := TailFile(f, -1); err != ErrNonPositiveLinesNumber {
		t.Fatalf("Expected ErrNonPositiveLinesNumber, got %v", err)
	}
	if _, err := TailFile(f, 0); err != ErrNonPositiveLinesNumber {
		t.Fatalf("Expected ErrNonPositiveLinesNumber, got %s", err)
	}
}

func BenchmarkTail(b *testing.B) {
	f, err := os.CreateTemp("", "tail-test")
	if err != nil {
		b.Fatal(err)
	}
	defer f.Close()
	defer os.RemoveAll(f.Name())
	for i := 0; i < 10000; i++ {
		if _, err := f.WriteString("tailfile pretty interesting line\n"); err != nil {
			b.Fatal(err)
		}
	}
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		if _, err := TailFile(f, 1000); err != nil {
			b.Fatal(err)
		}
	}
}

func TestNewTailReader(t *testing.T) {
	t.Parallel()
	ctx := context.Background()

	for dName, delim := range map[string][]byte{
		"no delimiter":          {},
		"single byte delimiter": {'\n'},
		"2 byte delimiter":      []byte(";\n"),
		"4 byte delimiter":      []byte("####"),
		"8 byte delimiter":      []byte("########"),
		"12 byte delimiter":     []byte("############"),
	} {
		t.Run(dName, func(t *testing.T) {
			delim := delim
			t.Parallel()

			s1 := "Hello world."
			s2 := "Today is a fine day."
			s3 := "So long, and thanks for all the fish!"
			s4 := strings.Repeat("a", blockSize/2) // same as block size
			s5 := strings.Repeat("a", blockSize)   // just to make sure
			s6 := strings.Repeat("a", blockSize*2) // bigger than block size
			s7 := strings.Repeat("a", blockSize-1) // single line same as block

			s8 := `{"log":"Don't panic!\n","stream":"stdout","time":"2018-04-04T20:28:44.7207062Z"}`
			jsonTest := make([]string, 0, 20)
			for i := 0; i < 20; i++ {
				jsonTest = append(jsonTest, s8)
			}

			for _, test := range []struct {
				desc string
				data []string
			}{
				{desc: "one small entry", data: []string{s1}},
				{desc: "several small entries", data: []string{s1, s2, s3}},
				{desc: "various sizes", data: []string{s1, s2, s3, s4, s5, s1, s2, s3, s7, s6}},
				{desc: "multiple lines with one more than block", data: []string{s5, s5, s5, s5, s5}},
				{desc: "multiple lines much bigger than block", data: []string{s6, s6, s6, s6, s6}},
				{desc: "multiple lines same as block", data: []string{s4, s4, s4, s4, s4}},
				{desc: "single line same as block", data: []string{s7}},
				{desc: "single line half block", data: []string{s4}},
				{desc: "single line twice block", data: []string{s6}},
				{desc: "json encoded values", data: jsonTest},
				{desc: "no lines", data: []string{}},
				{desc: "same length as delimiter", data: []string{strings.Repeat("a", len(delim))}},
			} {
				t.Run(test.desc, func(t *testing.T) {
					test := test
					t.Parallel()

					maxLen := len(test.data)
					if maxLen > 10 {
						maxLen = 10
					}

					s := strings.Join(test.data, string(delim))
					if len(test.data) > 0 {
						s += string(delim)
					}

					for i := 1; i <= maxLen; i++ {
						t.Run(fmt.Sprintf("%d lines", i), func(t *testing.T) {
							i := i
							t.Parallel()

							r := strings.NewReader(s)
							tr, lines, err := NewTailReaderWithDelimiter(ctx, r, i, delim)
							if len(delim) == 0 {
								assert.Assert(t, err != nil)
								assert.Assert(t, lines == 0)
								return
							}
							assert.NilError(t, err)
							assert.Check(t, lines == i, "%d -- %d", lines, i)

							b, err := io.ReadAll(tr)
							assert.NilError(t, err)

							expectLines := test.data[len(test.data)-i:]
							assert.Check(t, len(expectLines) == i)
							expect := strings.Join(expectLines, string(delim)) + string(delim)
							assert.Check(t, string(b) == expect, "\n%v\n%v", b, []byte(expect))
						})
					}

					t.Run("request more lines than available", func(t *testing.T) {
						t.Parallel()

						r := strings.NewReader(s)
						tr, lines, err := NewTailReaderWithDelimiter(ctx, r, len(test.data)*2, delim)
						if len(delim) == 0 {
							assert.Assert(t, err != nil)
							assert.Assert(t, lines == 0)
							return
						}
						if len(test.data) == 0 {
							assert.Assert(t, err == ErrNonPositiveLinesNumber, err)
							return
						}

						assert.NilError(t, err)
						assert.Check(t, lines == len(test.data), "%d -- %d", lines, len(test.data))
						b, err := io.ReadAll(tr)
						assert.NilError(t, err)
						assert.Check(t, bytes.Equal(b, []byte(s)), "\n%v\n%v", b, []byte(s))
					})
				})
			}
		})
	}
	t.Run("truncated last line", func(t *testing.T) {
		t.Run("more than available", func(t *testing.T) {
			tail, nLines, err := NewTailReader(ctx, strings.NewReader("a\nb\nextra"), 3)
			assert.NilError(t, err)
			assert.Check(t, nLines == 2, nLines)

			rdr := bufio.NewReader(tail)
			data, _, err := rdr.ReadLine()
			assert.NilError(t, err)
			assert.Check(t, string(data) == "a", string(data))

			data, _, err = rdr.ReadLine()
			assert.NilError(t, err)
			assert.Check(t, string(data) == "b", string(data))

			_, _, err = rdr.ReadLine()
			assert.Assert(t, err == io.EOF, err)
		})
	})
	t.Run("truncated last line", func(t *testing.T) {
		t.Run("exact", func(t *testing.T) {
			tail, nLines, err := NewTailReader(ctx, strings.NewReader("a\nb\nextra"), 2)
			assert.NilError(t, err)
			assert.Check(t, nLines == 2, nLines)

			rdr := bufio.NewReader(tail)
			data, _, err := rdr.ReadLine()
			assert.NilError(t, err)
			assert.Check(t, string(data) == "a", string(data))

			data, _, err = rdr.ReadLine()
			assert.NilError(t, err)
			assert.Check(t, string(data) == "b", string(data))

			_, _, err = rdr.ReadLine()
			assert.Assert(t, err == io.EOF, err)
		})
	})

	t.Run("truncated last line", func(t *testing.T) {
		t.Run("one line", func(t *testing.T) {
			tail, nLines, err := NewTailReader(ctx, strings.NewReader("a\nb\nextra"), 1)
			assert.NilError(t, err)
			assert.Check(t, nLines == 1, nLines)

			rdr := bufio.NewReader(tail)
			data, _, err := rdr.ReadLine()
			assert.NilError(t, err)
			assert.Check(t, string(data) == "b", string(data))

			_, _, err = rdr.ReadLine()
			assert.Assert(t, err == io.EOF, err)
		})
	})
}
