package rotatelogs_test

import (
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"github.com/jonboulle/clockwork"
	rotatelogs "github.com/lestrrat-go/file-rotatelogs"
	"github.com/pkg/errors"
	"github.com/stretchr/testify/assert"
)

func TestSatisfiesIOWriter(t *testing.T) {
	var w io.Writer
	w, _ = rotatelogs.New("/foo/bar")
	_ = w
}

func TestSatisfiesIOCloser(t *testing.T) {
	var c io.Closer
	c, _ = rotatelogs.New("/foo/bar")
	_ = c
}

func TestLogRotate(t *testing.T) {
	dir, err := ioutil.TempDir("", "file-rotatelogs-test")
	if !assert.NoError(t, err, "creating temporary directory should succeed") {
		return
	}
	defer os.RemoveAll(dir)

	// Change current time, so we can safely purge old logs
	dummyTime := time.Now().Add(-7 * 24 * time.Hour)
	dummyTime = dummyTime.Add(time.Duration(-1 * dummyTime.Nanosecond()))
	clock := clockwork.NewFakeClockAt(dummyTime)
	linkName := filepath.Join(dir, "log")
	rl, err := rotatelogs.New(
		filepath.Join(dir, "log%Y%m%d%H%M%S"),
		rotatelogs.WithClock(clock),
		rotatelogs.WithMaxAge(24*time.Hour),
		rotatelogs.WithLinkName(linkName),
	)
	if !assert.NoError(t, err, `rotatelogs.New should succeed`) {
		return
	}
	defer rl.Close()

	str := "Hello, World"
	n, err := rl.Write([]byte(str))
	if !assert.NoError(t, err, "rl.Write should succeed") {
		return
	}

	if !assert.Len(t, str, n, "rl.Write should succeed") {
		return
	}

	fn := rl.CurrentFileName()
	if fn == "" {
		t.Errorf("Could not get filename %s", fn)
	}

	content, err := ioutil.ReadFile(fn)
	if err != nil {
		t.Errorf("Failed to read file %s: %s", fn, err)
	}

	if string(content) != str {
		t.Errorf(`File content does not match (was "%s")`, content)
	}

	err = os.Chtimes(fn, dummyTime, dummyTime)
	if err != nil {
		t.Errorf("Failed to change access/modification times for %s: %s", fn, err)
	}

	fi, err := os.Stat(fn)
	if err != nil {
		t.Errorf("Failed to stat %s: %s", fn, err)
	}

	if !fi.ModTime().Equal(dummyTime) {
		t.Errorf("Failed to chtime for %s (expected %s, got %s)", fn, fi.ModTime(), dummyTime)
	}

	clock.Advance(time.Duration(7 * 24 * time.Hour))

	// This next Write() should trigger Rotate()
	rl.Write([]byte(str))
	newfn := rl.CurrentFileName()
	if newfn == fn {
		t.Errorf(`New file name and old file name should not match ("%s" != "%s")`, fn, newfn)
	}

	content, err = ioutil.ReadFile(newfn)
	if err != nil {
		t.Errorf("Failed to read file %s: %s", newfn, err)
	}

	if string(content) != str {
		t.Errorf(`File content does not match (was "%s")`, content)
	}

	time.Sleep(time.Second)

	// fn was declared above, before mocking CurrentTime
	// Old files should have been unlinked
	_, err = os.Stat(fn)
	if !assert.Error(t, err, "os.Stat should have failed") {
		return
	}

	linkDest, err := os.Readlink(linkName)
	if err != nil {
		t.Errorf("Failed to readlink %s: %s", linkName, err)
	}

	if linkDest != newfn {
		t.Errorf(`Symlink destination does not match expected filename ("%s" != "%s")`, newfn, linkDest)
	}
}

func CreateRotationTestFile(dir string, base time.Time, d time.Duration, n int) {
	timestamp := base
	for i := 0; i < n; i++ {
		// %Y%m%d%H%M%S
		suffix := timestamp.Format("20060102150405")
		path := filepath.Join(dir, "log"+suffix)
		ioutil.WriteFile(path, []byte("rotation test file\n"), os.ModePerm)
		os.Chtimes(path, timestamp, timestamp)
		timestamp = timestamp.Add(d)
	}
}

func TestLogRotationCount(t *testing.T) {
	dir, err := ioutil.TempDir("", "file-rotatelogs-rotationcount-test")
	if !assert.NoError(t, err, "creating temporary directory should succeed") {
		return
	}
	defer os.RemoveAll(dir)

	dummyTime := time.Now().Add(-7 * 24 * time.Hour)
	dummyTime = dummyTime.Add(time.Duration(-1 * dummyTime.Nanosecond()))
	clock := clockwork.NewFakeClockAt(dummyTime)

	t.Run("Either maxAge or rotationCount should be set", func(t *testing.T) {
		rl, err := rotatelogs.New(
			filepath.Join(dir, "log%Y%m%d%H%M%S"),
			rotatelogs.WithClock(clock),
			rotatelogs.WithMaxAge(time.Duration(0)),
			rotatelogs.WithRotationCount(0),
		)
		if !assert.NoError(t, err, `Both of maxAge and rotationCount is disabled`) {
			return
		}
		defer rl.Close()
	})

	t.Run("Either maxAge or rotationCount should be set", func(t *testing.T) {
		rl, err := rotatelogs.New(
			filepath.Join(dir, "log%Y%m%d%H%M%S"),
			rotatelogs.WithClock(clock),
			rotatelogs.WithMaxAge(1),
			rotatelogs.WithRotationCount(1),
		)
		if !assert.Error(t, err, `Both of maxAge and rotationCount is enabled`) {
			return
		}
		if rl != nil {
			defer rl.Close()
		}
	})

	t.Run("Only latest log file is kept", func(t *testing.T) {
		rl, err := rotatelogs.New(
			filepath.Join(dir, "log%Y%m%d%H%M%S"),
			rotatelogs.WithClock(clock),
			rotatelogs.WithMaxAge(-1),
			rotatelogs.WithRotationCount(1),
		)
		if !assert.NoError(t, err, `rotatelogs.New should succeed`) {
			return
		}
		defer rl.Close()

		n, err := rl.Write([]byte("dummy"))
		if !assert.NoError(t, err, "rl.Write should succeed") {
			return
		}
		if !assert.Len(t, "dummy", n, "rl.Write should succeed") {
			return
		}
		time.Sleep(time.Second)
		files, err := filepath.Glob(filepath.Join(dir, "log*"))
		if !assert.Equal(t, 1, len(files), "Only latest log is kept") {
			return
		}
	})

	t.Run("Old log files are purged except 2 log files", func(t *testing.T) {
		CreateRotationTestFile(dir, dummyTime, time.Duration(time.Hour), 5)
		rl, err := rotatelogs.New(
			filepath.Join(dir, "log%Y%m%d%H%M%S"),
			rotatelogs.WithClock(clock),
			rotatelogs.WithMaxAge(-1),
			rotatelogs.WithRotationCount(2),
		)
		if !assert.NoError(t, err, `rotatelogs.New should succeed`) {
			return
		}
		defer rl.Close()

		n, err := rl.Write([]byte("dummy"))
		if !assert.NoError(t, err, "rl.Write should succeed") {
			return
		}
		if !assert.Len(t, "dummy", n, "rl.Write should succeed") {
			return
		}
		time.Sleep(time.Second)
		files, err := filepath.Glob(filepath.Join(dir, "log*"))
		if !assert.Equal(t, 2, len(files), "One file is kept") {
			return
		}
	})

}

func TestLogSetOutput(t *testing.T) {
	dir, err := ioutil.TempDir("", "file-rotatelogs-test")
	if err != nil {
		t.Errorf("Failed to create temporary directory: %s", err)
	}
	defer os.RemoveAll(dir)

	rl, err := rotatelogs.New(filepath.Join(dir, "log%Y%m%d%H%M%S"))
	if !assert.NoError(t, err, `rotatelogs.New should succeed`) {
		return
	}
	defer rl.Close()

	log.SetOutput(rl)
	defer log.SetOutput(os.Stderr)

	str := "Hello, World"
	log.Print(str)

	fn := rl.CurrentFileName()
	if fn == "" {
		t.Errorf("Could not get filename %s", fn)
	}

	content, err := ioutil.ReadFile(fn)
	if err != nil {
		t.Errorf("Failed to read file %s: %s", fn, err)
	}

	if !strings.Contains(string(content), str) {
		t.Errorf(`File content does not contain "%s" (was "%s")`, str, content)
	}
}

func TestGHIssue16(t *testing.T) {
	defer func() {
		if v := recover(); v != nil {
			assert.NoError(t, errors.Errorf("%s", v), "error should be nil")
		}
	}()

	dir, err := ioutil.TempDir("", "file-rotatelogs-gh16")
	if !assert.NoError(t, err, `creating temporary directory should succeed`) {
		return
	}
	defer os.RemoveAll(dir)

	rl, err := rotatelogs.New(
		filepath.Join(dir, "log%Y%m%d%H%M%S"),
		rotatelogs.WithLinkName("./test.log"),
		rotatelogs.WithRotationTime(10*time.Second),
		rotatelogs.WithRotationCount(3),
		rotatelogs.WithMaxAge(-1),
	)
	if !assert.NoError(t, err, `rotatelogs.New should succeed`) {
		return
	}

	if !assert.NoError(t, rl.Rotate(), "rl.Rotate should succeed") {
		return
	}
	defer rl.Close()
}

func TestRotationGenerationalNames(t *testing.T) {
	dir, err := ioutil.TempDir("", "file-rotatelogs-generational")
	if !assert.NoError(t, err, `creating temporary directory should succeed`) {
		return
	}
	defer os.RemoveAll(dir)

	t.Run("Rotate over unchanged pattern", func(t *testing.T) {
		rl, err := rotatelogs.New(
			filepath.Join(dir, "unchaged-pattern.log"),
		)
		if !assert.NoError(t, err, `rotatelogs.New should succeed`) {
			return
		}

		seen := map[string]struct{}{}
		for i := 0; i < 10; i++ {
			rl.Write([]byte("Hello, World!"))
			if !assert.NoError(t, rl.Rotate(), "rl.Rotate should succeed") {
				return
			}

			// Because every call to Rotate should yield a new log file,
			// and the previous files already exist, the filenames should share
			// the same prefix and have a unique suffix
			fn := filepath.Base(rl.CurrentFileName())
			if !assert.True(t, strings.HasPrefix(fn, "unchaged-pattern.log"), "prefix for all filenames should match") {
				return
			}
			suffix := strings.TrimPrefix(fn, "unchanged-pattern.log")
			if _, ok := seen[suffix]; !assert.False(t, ok, `filename suffix %s should be unique`, suffix) {
				return
			}
			seen[suffix] = struct{}{}
		}
		defer rl.Close()
	})
	t.Run("Rotate over pattern change over every second", func(t *testing.T) {
		rl, err := rotatelogs.New(
			filepath.Join(dir, "every-second-pattern-%Y%m%d%H%M%S.log"),
			rotatelogs.WithRotationTime(time.Nanosecond),
		)
		if !assert.NoError(t, err, `rotatelogs.New should succeed`) {
			return
		}

		for i := 0; i < 10; i++ {
			time.Sleep(time.Second)
			rl.Write([]byte("Hello, World!"))
			if !assert.NoError(t, rl.Rotate(), "rl.Rotate should succeed") {
				return
			}

			// because every new Write should yield a new logfile,
			// every rorate should be create a filename ending with a .1
			if !assert.True(t, strings.HasSuffix(rl.CurrentFileName(), ".1"), "log name should end with .1") {
				return
			}
		}
		defer rl.Close()
	})
}

type ClockFunc func() time.Time

func (f ClockFunc) Now() time.Time {
	return f()
}

func TestGHIssue23(t *testing.T) {
	dir, err := ioutil.TempDir("", "file-rotatelogs-generational")
	if !assert.NoError(t, err, `creating temporary directory should succeed`) {
		return
	}
	defer os.RemoveAll(dir)

	for _, locName := range []string{"Asia/Tokyo", "Pacific/Honolulu"} {
		loc, _ := time.LoadLocation(locName)
		tests := []struct {
			Expected string
			Clock    rotatelogs.Clock
		}{
			{
				Expected: filepath.Join(dir, strings.ToLower(strings.Replace(locName, "/", "_", -1)) + ".201806010000.log"),
				Clock: ClockFunc(func() time.Time {
					return time.Date(2018, 6, 1, 3, 18, 0, 0, loc)
				}),
			},
			{
				Expected: filepath.Join(dir, strings.ToLower(strings.Replace(locName, "/", "_", -1)) + ".201712310000.log"),
				Clock: ClockFunc(func() time.Time {
					return time.Date(2017, 12, 31, 23, 52, 0, 0, loc)
				}),
			},
		}
		for _, test := range tests {
			t.Run(fmt.Sprintf("location = %s, time = %s", locName, test.Clock.Now().Format(time.RFC3339)), func(t *testing.T) {
				template := strings.ToLower(strings.Replace(locName, "/", "_", -1)) + ".%Y%m%d%H%M.log"
				rl, err := rotatelogs.New(
					filepath.Join(dir, template),
					rotatelogs.WithClock(test.Clock), // we're not using WithLocation, but it's the same thing
				)
				if !assert.NoError(t, err, "rotatelogs.New should succeed") {
					return
				}

				t.Logf("expected %s", test.Expected)
				rl.Rotate()
				if !assert.Equal(t, test.Expected, rl.CurrentFileName(), "file names should match") {
					return
				}
			})
		}
	}
}
