// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package logging

import (
	"fmt"
	"math/rand"
	"os"
	"path/filepath"
	"testing"

	"github.com/hashicorp/nomad/helper/testlog"
	"github.com/hashicorp/nomad/testutil"
	"github.com/stretchr/testify/require"
	"go.uber.org/goleak"
)

var (
	baseFileName = "redis.stdout"
)

func TestFileRotator_IncorrectPath(t *testing.T) {
	defer goleak.VerifyNone(t)

	_, err := NewFileRotator("/foo", baseFileName, 10, 10, testlog.HCLogger(t))
	require.Error(t, err)
	require.Contains(t, err.Error(), "no such file or directory")
}

func TestFileRotator_CreateNewFile(t *testing.T) {
	defer goleak.VerifyNone(t)

	path := t.TempDir()

	fr, err := NewFileRotator(path, baseFileName, 10, 10, testlog.HCLogger(t))
	require.NoError(t, err)
	defer fr.Close()

	_, err = os.Stat(filepath.Join(path, "redis.stdout.0"))
	require.NoError(t, err)
}

func TestFileRotator_OpenLastFile(t *testing.T) {
	defer goleak.VerifyNone(t)

	path := t.TempDir()

	fname1 := filepath.Join(path, "redis.stdout.0")
	fname2 := filepath.Join(path, "redis.stdout.2")

	f1, err := os.Create(fname1)
	require.NoError(t, err)
	f1.Close()

	f2, err := os.Create(fname2)
	require.NoError(t, err)
	f2.Close()

	fr, err := NewFileRotator(path, baseFileName, 10, 10, testlog.HCLogger(t))
	require.NoError(t, err)
	defer fr.Close()

	require.Equal(t, fname2, fr.currentFile.Name())
}

func TestFileRotator_WriteToCurrentFile(t *testing.T) {
	defer goleak.VerifyNone(t)

	path := t.TempDir()

	fname1 := filepath.Join(path, "redis.stdout.0")
	f1, err := os.Create(fname1)
	require.NoError(t, err)
	f1.Close()

	fr, err := NewFileRotator(path, baseFileName, 10, 5, testlog.HCLogger(t))
	require.NoError(t, err)
	defer fr.Close()

	fr.Write([]byte("abcde"))

	testutil.WaitForResult(func() (bool, error) {
		fi, err := os.Stat(fname1)
		if err != nil {
			return false, fmt.Errorf("failed to stat file %v: %w", fname1, err)
		}
		actual := fi.Size()
		if actual != 5 {
			return false, fmt.Errorf("expected size %d but found %d", 5, actual)
		}

		return true, nil
	}, func(err error) {
		require.NoError(t, err)
	})
}

func TestFileRotator_RotateFiles(t *testing.T) {
	defer goleak.VerifyNone(t)

	path := t.TempDir()

	fr, err := NewFileRotator(path, baseFileName, 10, 5, testlog.HCLogger(t))
	require.NoError(t, err)
	defer fr.Close()

	str := "abcdefgh"
	nw, err := fr.Write([]byte(str))
	require.NoError(t, err)
	require.Equal(t, len(str), nw)

	testutil.WaitForResult(func() (bool, error) {
		fname1 := filepath.Join(path, "redis.stdout.0")
		fi, err := os.Stat(fname1)
		if err != nil {
			return false, fmt.Errorf("failed to stat file %v: %w", fname1, err)
		}
		if fi.Size() != 5 {
			return false, fmt.Errorf("expected size: %v, actual: %v", 5, fi.Size())
		}

		fname2 := filepath.Join(path, "redis.stdout.1")
		if _, err := os.Stat(fname2); err != nil {
			return false, fmt.Errorf("expected file %v to exist", fname2)
		}

		if fi2, err := os.Stat(fname2); err == nil {
			if fi2.Size() != 3 {
				return false, fmt.Errorf("expected size: %v, actual: %v", 3, fi2.Size())
			}
		} else {
			return false, fmt.Errorf("error getting the file info: %v", err)
		}

		return true, nil
	}, func(err error) {
		require.NoError(t, err)
	})
}

func TestFileRotator_RotateFiles_Boundary(t *testing.T) {
	defer goleak.VerifyNone(t)

	path := t.TempDir()

	fr, err := NewFileRotator(path, baseFileName, 10, 5, testlog.HCLogger(t))
	require.NoError(t, err)
	defer fr.Close()

	// We will write three times:
	// 1st: Write with new lines spanning two files
	// 2nd: Write long string with no new lines
	// 3rd: Write a single new line
	expectations := [][]byte{
		[]byte("ab\n"),
		[]byte("cdef\n"),
		[]byte("12345"),
		[]byte("67890"),
		[]byte("\n"),
	}

	for _, str := range []string{"ab\ncdef\n", "1234567890", "\n"} {
		nw, err := fr.Write([]byte(str))
		require.NoError(t, err)
		require.Equal(t, len(str), nw)
	}

	testutil.WaitForResult(func() (bool, error) {

		for i, exp := range expectations {
			fname := filepath.Join(path, fmt.Sprintf("redis.stdout.%d", i))
			fi, err := os.Stat(fname)
			if err != nil {
				return false, fmt.Errorf("failed to stat file %v: %w", fname, err)
			}
			if int(fi.Size()) != len(exp) {
				return false, fmt.Errorf("expected size: %v, actual: %v", len(exp), fi.Size())
			}
		}

		return true, nil
	}, func(err error) {
		require.NoError(t, err)
	})
}

func TestFileRotator_WriteRemaining(t *testing.T) {
	defer goleak.VerifyNone(t)

	path := t.TempDir()

	fname1 := filepath.Join(path, "redis.stdout.0")
	err := os.WriteFile(fname1, []byte("abcd"), 0600)
	require.NoError(t, err)

	fr, err := NewFileRotator(path, baseFileName, 10, 5, testlog.HCLogger(t))
	require.NoError(t, err)
	defer fr.Close()

	str := "efghijkl"
	nw, err := fr.Write([]byte(str))
	require.NoError(t, err)
	require.Equal(t, len(str), nw)

	testutil.WaitForResult(func() (bool, error) {
		fi, err := os.Stat(fname1)
		if err != nil {
			return false, fmt.Errorf("error getting the file info: %v", err)
		}
		if fi.Size() != 5 {
			return false, fmt.Errorf("expected size: %v, actual: %v", 5, fi.Size())
		}

		fname2 := filepath.Join(path, "redis.stdout.1")
		if _, err := os.Stat(fname2); err != nil {
			return false, fmt.Errorf("expected file %v to exist", fname2)
		}

		if fi2, err := os.Stat(fname2); err == nil {
			if fi2.Size() != 5 {
				return false, fmt.Errorf("expected size: %v, actual: %v", 5, fi2.Size())
			}
		} else {
			return false, fmt.Errorf("error getting the file info: %v", err)
		}

		fname3 := filepath.Join(path, "redis.stdout.2")
		if _, err := os.Stat(fname3); err != nil {
			return false, fmt.Errorf("expected file %v to exist", fname3)
		}

		if fi3, err := os.Stat(fname3); err == nil {
			if fi3.Size() != 2 {
				return false, fmt.Errorf("expected size: %v, actual: %v", 2, fi3.Size())
			}
		} else {
			return false, fmt.Errorf("error getting the file info: %v", err)
		}

		return true, nil
	}, func(err error) {
		require.NoError(t, err)
	})

}

func TestFileRotator_PurgeOldFiles(t *testing.T) {
	defer goleak.VerifyNone(t)

	path := t.TempDir()

	fr, err := NewFileRotator(path, baseFileName, 2, 2, testlog.HCLogger(t))
	require.NoError(t, err)
	defer fr.Close()

	str := "abcdeghijklmn"
	nw, err := fr.Write([]byte(str))
	require.NoError(t, err)
	require.Equal(t, len(str), nw)

	testutil.WaitForResult(func() (bool, error) {
		f, err := os.ReadDir(path)
		if err != nil {
			return false, fmt.Errorf("failed to read dir %v: %w", path, err)
		}

		if len(f) != 2 {
			return false, fmt.Errorf("expected number of files: %v, got: %v %v", 2, len(f), f)
		}

		return true, nil
	}, func(err error) {
		require.NoError(t, err)
	})
}

func BenchmarkRotator(b *testing.B) {
	kb := 1024
	for _, inputSize := range []int{kb, 2 * kb, 4 * kb, 8 * kb, 16 * kb, 32 * kb, 64 * kb, 128 * kb, 256 * kb} {
		b.Run(fmt.Sprintf("%dKB", inputSize/kb), func(b *testing.B) {
			benchmarkRotatorWithInputSize(inputSize, b)
		})
	}
}

func benchmarkRotatorWithInputSize(size int, b *testing.B) {
	path := b.TempDir()

	fr, err := NewFileRotator(path, baseFileName, 5, 1024*1024, testlog.HCLogger(b))
	require.NoError(b, err)
	defer fr.Close()

	b.ResetTimer()

	// run the Fib function b.N times
	for n := 0; n < b.N; n++ {
		// Generate some input
		data := make([]byte, size)
		_, err := rand.Read(data)
		require.NoError(b, err)

		// Insert random new lines
		for i := 0; i < 100; i++ {
			index := rand.Intn(size)
			data[index] = '\n'
		}

		// Write the data
		_, err = fr.Write(data)
		require.NoError(b, err)
	}
}
