package m

import (
	"math"
	"os"
	"os/exec"
	"path"
	"runtime"
	"strconv"
	"strings"
	"testing"

	"github.com/alecthomas/chroma/v2/formatters"
	"github.com/alecthomas/chroma/v2/styles"
	"gotest.tools/v3/assert"
)

func testGetLineCount(t *testing.T, reader *Reader) {
	if strings.Contains(*reader.name, "compressed") {
		// We are no good at counting lines of compressed files, never mind
		return
	}

	cmd := exec.Command("wc", "-l", *reader.name)
	output, err := cmd.CombinedOutput()
	if err != nil {
		t.Error("Error calling wc -l to count lines of", *reader.name, err)
	}

	wcNumberString := strings.Split(strings.TrimSpace(string(output)), " ")[0]
	wcLineCount, err := strconv.Atoi(wcNumberString)
	if err != nil {
		t.Error("Error counting lines of", *reader.name, err)
	}

	if strings.HasSuffix(*reader.name, "/line-without-newline.txt") {
		// "wc -l" thinks this file contains zero lines
		wcLineCount = 1
	} else if strings.HasSuffix(*reader.name, "/two-lines-no-trailing-newline.txt") {
		// "wc -l" thinks this file contains one line
		wcLineCount = 2
	}

	if reader.GetLineCount() != wcLineCount {
		t.Errorf("Got %d lines from the reader but %d lines from wc -l: <%s>",
			reader.GetLineCount(), wcLineCount, *reader.name)
	}

	countLinesCount, err := countLines(*reader.name)
	if err != nil {
		panic(err)
	}
	if countLinesCount != uint64(wcLineCount) {
		t.Errorf("Got %d lines from wc -l, but %d lines from our countLines() function", wcLineCount, countLinesCount)
	}
}

func testGetLines(t *testing.T, reader *Reader) {
	lines, _ := reader.GetLines(1, 10)
	if len(lines.lines) > 10 {
		t.Errorf("Asked for 10 lines, got too many: %d", len(lines.lines))
	}

	if len(lines.lines) < 10 {
		// No good plan for how to test short files, more than just
		// querying them, which we just did
		return
	}

	// Test clipping at the end
	lines, _ = reader.GetLines(math.MaxInt32, 10)
	if len(lines.lines) != 10 {
		t.Errorf("Asked for 10 lines but got %d", len(lines.lines))
		return
	}

	startOfLastSection := lines.firstLineOneBased
	lines, _ = reader.GetLines(startOfLastSection, 10)
	if lines.firstLineOneBased != startOfLastSection {
		t.Errorf("Expected start line %d when asking for the last 10 lines, got %d",
			startOfLastSection, lines.firstLineOneBased)
		return
	}
	if len(lines.lines) != 10 {
		t.Errorf("Expected 10 lines when asking for the last 10 lines, got %d",
			len(lines.lines))
		return
	}

	lines, _ = reader.GetLines(startOfLastSection+1, 10)
	if lines.firstLineOneBased != startOfLastSection {
		t.Errorf("Expected start line %d when asking for the last+1 10 lines, got %d",
			startOfLastSection, lines.firstLineOneBased)
		return
	}
	if len(lines.lines) != 10 {
		t.Errorf("Expected 10 lines when asking for the last+1 10 lines, got %d",
			len(lines.lines))
		return
	}

	lines, _ = reader.GetLines(startOfLastSection-1, 10)
	if lines.firstLineOneBased != startOfLastSection-1 {
		t.Errorf("Expected start line %d when asking for the last-1 10 lines, got %d",
			startOfLastSection, lines.firstLineOneBased)
		return
	}
	if len(lines.lines) != 10 {
		t.Errorf("Expected 10 lines when asking for the last-1 10 lines, got %d",
			len(lines.lines))
		return
	}
}

func getSamplesDir() string {
	// From: https://coderwall.com/p/_fmbug/go-get-path-to-current-file
	_, filename, _, ok := runtime.Caller(0)
	if !ok {
		panic("Getting current filename failed")
	}

	return path.Join(path.Dir(filename), "../sample-files")
}

func getTestFiles() []string {
	files, err := os.ReadDir(getSamplesDir())
	if err != nil {
		panic(err)
	}

	var filenames []string
	for _, file := range files {
		filenames = append(filenames, "../sample-files/"+file.Name())
	}

	return filenames
}

// Wait for reader to finish reading and highlighting. Used by tests.
func (r *Reader) _wait() error {
	// Wait for our goroutine to finish
	for !r.done.Load() {
	}
	for !r.highlightingDone.Load() {
	}

	r.Lock()
	defer r.Unlock()
	return r.err
}

func TestGetLines(t *testing.T) {
	for _, file := range getTestFiles() {
		if strings.HasSuffix(file, ".xz") {
			_, err := exec.LookPath("xz")
			if err != nil {
				t.Log("Not testing xz compressed file, xz not found in $PATH: ", file)
				continue
			}
		}

		if strings.HasSuffix(file, ".bz2") {
			_, err := exec.LookPath("bzip2")
			if err != nil {
				t.Log("Not testing bzip2 compressed file, bzip2 not found in $PATH: ", file)
				continue
			}
		}

		reader, err := NewReaderFromFilename(file, *styles.Get("native"), formatters.TTY16m)
		if err != nil {
			t.Errorf("Error opening file <%s>: %s", file, err.Error())
			continue
		}
		if err := reader._wait(); err != nil {
			t.Errorf("Error reading file <%s>: %s", file, err.Error())
			continue
		}

		t.Run(file, func(t *testing.T) {
			testGetLines(t, reader)
			testGetLineCount(t, reader)
			testHighlightingLineCount(t, file)
		})
	}
}

func testHighlightingLineCount(t *testing.T, filenameWithPath string) {
	// This won't work on compressed files
	if strings.HasSuffix(filenameWithPath, ".xz") {
		return
	}
	if strings.HasSuffix(filenameWithPath, ".bz2") {
		return
	}
	if strings.HasSuffix(filenameWithPath, ".gz") {
		return
	}

	// Load the unformatted file
	rawBytes, err := os.ReadFile(filenameWithPath)
	if err != nil {
		panic(err)
	}
	rawContents := string(rawBytes)

	// Count its lines
	rawLinefeedsCount := strings.Count(rawContents, "\n")
	rawRunes := []rune(rawContents)
	rawFileEndsWithNewline := true // Special case empty files
	if len(rawRunes) > 0 {
		rawFileEndsWithNewline = rawRunes[len(rawRunes)-1] == '\n'
	}
	rawLinesCount := rawLinefeedsCount
	if !rawFileEndsWithNewline {
		rawLinesCount += 1
	}

	// Then load the same file using one of our Readers
	reader, err := NewReaderFromFilename(filenameWithPath, *styles.Get("native"), formatters.TTY16m)
	if err != nil {
		panic(err)
	}
	err = reader._wait()
	if err != nil {
		panic(err)
	}

	highlightedLinesCount := reader.GetLineCount()
	assert.Check(t, rawLinesCount == highlightedLinesCount)
}

func TestGetLongLine(t *testing.T) {
	file := "../sample-files/very-long-line.txt"
	reader, err := NewReaderFromFilename(file, *styles.Get("native"), formatters.TTY16m)
	if err != nil {
		panic(err)
	}
	if err := reader._wait(); err != nil {
		panic(err)
	}

	lines, overflow := reader.GetLines(1, 5)
	assert.Equal(t, lines.firstLineOneBased, 1)
	assert.Equal(t, len(lines.lines), 1)

	// This fits because we got all (one) input lines. Given the line length the
	// line is unlikely to fit on screen, but that's not what this didFit is
	// about.
	assert.Equal(t, overflow, didFit)

	line := lines.lines[0]
	assert.Assert(t, strings.HasPrefix(line.Plain(), "1 2 3 4"), "<%s>", line)
	assert.Assert(t, strings.HasSuffix(line.Plain(), "0123456789"), line)

	assert.Equal(t, len(line.Plain()), 100021)
}

func getReaderWithLineCount(totalLines int) *Reader {
	reader := NewReaderFromStream("", strings.NewReader(strings.Repeat("x\n", totalLines)))
	if err := reader._wait(); err != nil {
		panic(err)
	}

	return reader
}

func testStatusText(t *testing.T, fromLine int, toLine int, totalLines int, expected string) {
	testMe := getReaderWithLineCount(totalLines)
	linesRequested := toLine - fromLine + 1
	lines, _ := testMe.GetLines(fromLine, linesRequested)
	statusText := lines.statusText
	assert.Equal(t, statusText, expected)
}

func TestStatusText(t *testing.T) {
	testStatusText(t, 1, 10, 20, "20 lines  50%")
	testStatusText(t, 1, 5, 5, "5 lines  100%")
	testStatusText(t, 998, 999, 1000, "1_000 lines  99%")

	testStatusText(t, 0, 0, 0, "<empty>")
	testStatusText(t, 1, 1, 1, "1 line  100%")

	// Test with filename
	testMe, err := NewReaderFromFilename(getSamplesDir()+"/empty", *styles.Get("native"), formatters.TTY16m)
	if err != nil {
		panic(err)
	}
	if err := testMe._wait(); err != nil {
		panic(err)
	}

	line, overflow := testMe.GetLines(0, 0)
	assert.Equal(t, line.statusText, "empty: <empty>")
	assert.Equal(t, overflow, didFit) // Empty always fits
}

func testCompressedFile(t *testing.T, filename string) {
	filenameWithPath := getSamplesDir() + "/" + filename
	reader, e := NewReaderFromFilename(filenameWithPath, *styles.Get("native"), formatters.TTY16m)
	if e != nil {
		t.Errorf("Error opening file <%s>: %s", filenameWithPath, e.Error())
		panic(e)
	}
	if err := reader._wait(); err != nil {
		panic(err)
	}

	lines, _ := reader.GetLines(1, 5)
	assert.Equal(t, lines.lines[0].Plain(), "This is a compressed file", "%s", filename)
}

func TestCompressedFiles(t *testing.T) {
	testCompressedFile(t, "compressed.txt.gz")

	_, err := exec.LookPath("bzip2")
	if err == nil {
		testCompressedFile(t, "compressed.txt.bz2")
	} else {
		t.Log("WARNING: bzip2 not found in path, not testing automatic bzip2 decompression")
	}

	_, err = exec.LookPath("xz")
	if err == nil {
		testCompressedFile(t, "compressed.txt.xz")
	} else {
		t.Log("WARNING: xz not found in path, not testing automatic xz decompression")
	}
}

func TestFilterNotInstalled(t *testing.T) {
	// FIXME: Test what happens if we try to use a filter that is not installed
}

func TestFilterFailure(t *testing.T) {
	// FIXME: Test what happens if the filter command fails because of bad command line options
}

func TestFilterPermissionDenied(t *testing.T) {
	// FIXME: Test what happens if the filter command fails because it can't access the requested file
}

func TestFilterFileNotFound(t *testing.T) {
	// What happens if the filter cannot read its input file?
	NonExistentPath := "/does-not-exist"

	reader, err := newReaderFromCommand(NonExistentPath, "cat")

	// Creating should be fine, it's waiting for it to finish that should fail.
	// Feel free to re-evaluate in the future.
	assert.Check(t, err == nil)

	err = reader._wait()
	assert.Check(t, err != nil)

	assert.Check(t, strings.Contains(err.Error(), NonExistentPath), err.Error())
}

func TestFilterNotAFile(t *testing.T) {
	// FIXME: Test what happens if the filter command fails because the target is not a file
}

// How long does it take to read a file?
//
// This can be slow due to highlighting.
//
// Run with: go test -run='^$' -bench=. . ./...
func BenchmarkReaderDone(b *testing.B) {
	filename := getSamplesDir() + "/../m/pager.go" // This is our longest .go file
	b.ResetTimer()
	for n := 0; n < b.N; n++ {
		// This is our longest .go file
		readMe, err := NewReaderFromFilename(filename, *styles.Get("native"), formatters.TTY16m)
		if err != nil {
			panic(err)
		}

		// Wait for the reader to finish
		for !readMe.done.Load() {
		}
		if readMe.err != nil {
			panic(readMe.err)
		}
	}
}

// Try loading a large file
func BenchmarkReadLargeFile(b *testing.B) {
	// Try loading a file this large
	const largeSizeBytes = 35_000_000

	// First, create it from something...
	input_filename := getSamplesDir() + "/../m/pager.go"
	contents, err := os.ReadFile(input_filename)
	if err != nil {
		panic(err)
	}

	testdir := b.TempDir()
	largeFileName := testdir + "/large-file"
	largeFile, err := os.Create(largeFileName)
	if err != nil {
		panic(err)
	}

	totalBytesWritten := 0
	for totalBytesWritten < largeSizeBytes {
		written, err := largeFile.Write(contents)
		if err != nil {
			panic(err)
		}

		totalBytesWritten += written
	}
	err = largeFile.Close()
	if err != nil {
		panic(err)
	}

	b.ResetTimer()
	for n := 0; n < b.N; n++ {
		readMe, err := NewReaderFromFilename(largeFileName, *styles.Get("native"), formatters.TTY16m)
		if err != nil {
			panic(err)
		}

		// Wait for the reader to finish
		for !readMe.done.Load() {
		}
		if readMe.err != nil {
			panic(readMe.err)
		}
	}
}

// Count lines in pager.go
func BenchmarkCountLines(b *testing.B) {
	// First, get some sample lines...
	input_filename := getSamplesDir() + "/../m/pager.go"
	contents, err := os.ReadFile(input_filename)
	if err != nil {
		panic(err)
	}

	testdir := b.TempDir()
	countFileName := testdir + "/count-file"
	countFile, err := os.Create(countFileName)
	if err != nil {
		panic(err)
	}

	// 1000x makes this take about 12ms on my machine right now. Before 1000x
	// the numbers fluctuated much more.
	for n := 0; n < b.N*1000; n++ {
		_, err := countFile.Write(contents)
		if err != nil {
			panic(err)
		}
	}
	err = countFile.Close()
	if err != nil {
		panic(err)
	}

	b.ResetTimer()
	_, err = countLines(countFileName)
	if err != nil {
		panic(err)
	}
}
