// Copyright 2014-2022 Ulrich Kunitz. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package lzma

import (
	"bufio"
	"bytes"
	"io"
	"io/ioutil"
	"log"
	"os"
	"path/filepath"
	"testing"
	"testing/iotest"
)

func TestNewReader(t *testing.T) {
	f, err := os.Open("examples/a.lzma")
	if err != nil {
		t.Fatalf("open examples/a.lzma: %s", err)
	}
	defer f.Close()
	_, err = NewReader(bufio.NewReader(f))
	if err != nil {
		t.Fatalf("NewReader: %s", err)
	}
}

const (
	dirname  = "examples"
	origname = "a.txt"
)

func readOrigFile(t *testing.T) []byte {
	orig, err := os.ReadFile(filepath.Join(dirname, origname))
	if err != nil {
		t.Fatalf("ReadFile: %s", err)
	}
	return orig
}

func testDecodeFile(t *testing.T, filename string, orig []byte) {
	pathname := filepath.Join(dirname, filename)
	f, err := os.Open(pathname)
	if err != nil {
		t.Fatalf("Open(%q): %s", pathname, err)
	}
	defer func() {
		if err = f.Close(); err != nil {
			t.Fatalf("f.Close() error %s", err)
		}
	}()
	t.Logf("file %s opened", filename)
	l, err := NewReader(bufio.NewReader(f))
	if err != nil {
		t.Fatalf("NewReader: %s", err)
	}
	decoded, err := ioutil.ReadAll(l)
	if err != nil {
		t.Fatalf("ReadAll: %s", err)
	}
	t.Logf("%s", decoded)
	if len(orig) != len(decoded) {
		t.Fatalf("length decoded is %d; want %d",
			len(decoded), len(orig))
	}
	if !bytes.Equal(orig, decoded) {
		t.Fatalf("decoded file differs from original")
	}
}

func TestReaderSimple(t *testing.T) {
	// DebugOn(os.Stderr)
	// defer DebugOff()

	testDecodeFile(t, "a.lzma", readOrigFile(t))
}

func TestReaderAll(t *testing.T) {
	dirname := "examples"
	dir, err := os.Open(dirname)
	if err != nil {
		t.Fatalf("Open: %s", err)
	}
	defer func() {
		if err := dir.Close(); err != nil {
			t.Fatalf("dir.Close() error %s", err)
		}
	}()
	all, err := dir.Readdirnames(0)
	if err != nil {
		t.Fatalf("Readdirnames: %s", err)
	}
	// filter now all file with the pattern "a*.lzma"
	files := make([]string, 0, len(all))
	for _, fn := range all {
		match, err := filepath.Match("a*.lzma", fn)
		if err != nil {
			t.Fatalf("Match: %s", err)
		}
		if match {
			files = append(files, fn)
		}
	}
	t.Log("files:", files)
	orig := readOrigFile(t)
	// actually test the files
	for _, fn := range files {
		testDecodeFile(t, fn, orig)
	}
}

func Example_reader() {
	f, err := os.Open("fox.lzma")
	if err != nil {
		log.Fatal(err)
	}
	// no need for defer; Fatal calls os.Exit(1) that doesn't execute deferred functions
	r, err := NewReader(bufio.NewReader(f))
	if err != nil {
		log.Fatal(err)
	}
	_, err = io.Copy(os.Stdout, r)
	if err != nil {
		log.Fatal(err)
	}
	if err := f.Close(); err != nil {
		log.Fatal(err)
	}
	// Output:
	// The quick brown fox jumps over the lazy dog.
}

type wrapTest struct {
	name string
	wrap func(io.Reader) io.Reader
}

func (w *wrapTest) testFile(t *testing.T, filename string, orig []byte) {
	pathname := filepath.Join(dirname, filename)
	f, err := os.Open(pathname)
	if err != nil {
		t.Fatalf("Open(\"%s\"): %s", pathname, err)
	}
	defer func() {
		if err := f.Close(); err != nil {
			log.Fatal(err)
		}
	}()
	t.Logf("%s file %s opened", w.name, filename)
	l, err := NewReader(w.wrap(f))
	if err != nil {
		t.Fatalf("%s NewReader: %s", w.name, err)
	}
	decoded, err := ioutil.ReadAll(l)
	if err != nil {
		t.Fatalf("%s ReadAll: %s", w.name, err)
	}
	t.Logf("%s", decoded)
	if len(orig) != len(decoded) {
		t.Fatalf("%s length decoded is %d; want %d",
			w.name, len(decoded), len(orig))
	}
	if !bytes.Equal(orig, decoded) {
		t.Fatalf("%s decoded file differs from original", w.name)
	}
}

func TestReaderWrap(t *testing.T) {
	tests := [...]wrapTest{
		{"DataErrReader", iotest.DataErrReader},
		{"HalfReader", iotest.HalfReader},
		{"OneByteReader", iotest.OneByteReader},
		// TimeOutReader would require buffer
	}
	orig := readOrigFile(t)
	for _, tst := range tests {
		tst.testFile(t, "a.lzma", orig)
	}
}

func TestReaderBadFiles(t *testing.T) {
	dirname := "examples"
	dir, err := os.Open(dirname)
	if err != nil {
		t.Fatalf("Open: %s", err)
	}
	defer func() {
		if err := dir.Close(); err != nil {
			t.Fatalf("dir.Close() error %s", err)
		}
	}()
	all, err := dir.Readdirnames(0)
	if err != nil {
		t.Fatalf("Readdirnames: %s", err)
	}
	// filter now all file with the pattern "bad*.lzma"
	files := make([]string, 0, len(all))
	for _, fn := range all {
		match, err := filepath.Match("bad*.lzma", fn)
		if err != nil {
			t.Fatalf("Match: %s", err)
		}
		if match {
			files = append(files, fn)
		}
	}
	t.Log("files:", files)
	for _, filename := range files {
		pathname := filepath.Join(dirname, filename)
		f, err := os.Open(pathname)
		if err != nil {
			t.Fatalf("Open(\"%s\"): %s", pathname, err)
		}
		defer func(f *os.File) {
			if err := f.Close(); err != nil {
				t.Fatalf("f.Close() error %s", err)
			}
		}(f)
		t.Logf("file %s opened", filename)
		l, err := NewReader(f)
		if err != nil {
			t.Fatalf("NewReader: %s", err)
		}
		decoded, err := ioutil.ReadAll(l)
		if err == nil {
			t.Errorf("ReadAll for %s: no error", filename)
			t.Logf("%s", decoded)
			continue
		}
		t.Logf("%s: error %s", filename, err)
	}
}

type repReader byte

func (r repReader) Read(p []byte) (n int, err error) {
	for i := range p {
		p[i] = byte(r)
	}
	return len(p), nil
}

func newRepReader(c byte, n int64) *io.LimitedReader {
	return &io.LimitedReader{R: repReader(c), N: n}
}

func newCodeReader(r io.Reader) *io.PipeReader {
	pr, pw := io.Pipe()
	go func() {
		bw := bufio.NewWriter(pw)
		lw, err := NewWriter(bw)
		if err != nil {
			log.Fatalf("NewWriter error %s", err)
		}
		if _, err = io.Copy(lw, r); err != nil {
			log.Fatalf("io.Copy error %s", err)
		}
		if err = lw.Close(); err != nil {
			log.Fatalf("lw.Close error %s", err)
		}
		if err = bw.Flush(); err != nil {
			log.Fatalf("bw.Flush() error %s", err)
		}
		if err = pw.CloseWithError(io.EOF); err != nil {
			log.Fatalf("pw.CloseWithError(io.EOF) error %s", err)
		}
	}()
	return pr
}

func TestReaderErrAgain(t *testing.T) {
	lengths := []int64{0, 128, 1024, 4095, 4096, 4097, 8191, 8192, 8193}
	buf := make([]byte, 128)
	const c = 'A'
	for _, n := range lengths {
		t.Logf("n: %d", n)
		pr := newCodeReader(newRepReader(c, n))
		r, err := NewReader(pr)
		if err != nil {
			t.Fatalf("NewReader(pr) error %s", err)
		}
		k := int64(0)
		for {
			m, err := r.Read(buf)
			k += int64(m)
			if err == io.EOF {
				break
			}
			if err != nil {
				t.Errorf("r.Read(buf) error %s", err)
				break
			}
			if m > len(buf) {
				t.Fatalf("r.Read(buf) %d; want <= %d", m,
					len(buf))
			}
			for i, b := range buf[:m] {
				if b != c {
					t.Fatalf("buf[%d]=%c; want %c", i, b,
						c)
				}
			}
		}
		if k != n {
			t.Errorf("Read %d bytes; want %d", k, n)
		}
	}
}

func TestMinDictSize(t *testing.T) {
	const file = "examples/a.txt"
	uncompressed, err := os.ReadFile(file)
	if err != nil {
		t.Fatalf("os.ReadFile(%q) error %s", file, err)
	}
	f := bytes.NewReader(uncompressed)

	buf := new(bytes.Buffer)
	cfg := WriterConfig{DictCap: 4096}
	w, err := cfg.NewWriter(buf)
	if err != nil {
		t.Fatalf("WriterConfig(%+v).NewWriter(buf) error %s", cfg, err)
	}
	defer w.Close()
	if _, err = io.Copy(w, f); err != nil {
		t.Fatalf("io.Copy(w, f) error %s", err)
	}
	if err = w.Close(); err != nil {
		t.Fatalf("w.Close() error %s", err)
	}

	compressed := buf.Bytes()
	putUint32LE(compressed[1:5], 0)

	z := bytes.NewReader(compressed)
	r, err := NewReader(z)
	if err != nil {
		t.Fatalf("NewReader(z) error %s", err)
	}
	u, err := io.ReadAll(r)
	if err != nil {
		t.Fatalf("io.ReadAll(r) error %s", err)
	}

	if !bytes.Equal(u, uncompressed) {
		t.Fatalf("got %q; want %q", u, uncompressed)
	}
}

func TestZeroPrefixIssue(t *testing.T) {
	files := []string{
		"examples/a.lzma",
		"examples/a_lp1_lc2_pb1.lzma",
		"examples/a_eos_and_size.lzma",
		"fox.lzma",
	}

	zeroPrefix := []byte{0}
	rcfg := ReaderConfig{}

	for _, tc := range files {
		t.Run(tc, func(t *testing.T) {
			f, err := os.Open(tc)
			if err != nil {
				t.Fatalf("Open(%q) error %s", tc, err)
			}
			defer f.Close()
			zp := bytes.NewReader(zeroPrefix)
			z := io.MultiReader(zp, f)
			l, err := rcfg.NewReader(z)
			if err != nil {
				t.Logf("NewReader(z) for %s error %s", tc, err)
				return
			}
			h, ok := l.Header()
			t.Logf("Header %+v ok %v", h, ok)
			actualDictSize := len(l.d.Dict.buf.data) - 1
			t.Logf("Actual dictionary size: %d", actualDictSize)
			if actualDictSize > MinDictCap && h.Size >= 0 &&
				h.Size < int64(actualDictSize) {
				t.Errorf("actualDictSize %d smaller than file size %d",
					actualDictSize, h.Size)
			}
			_, err = io.ReadAll(l)
			if err == nil {
				t.Errorf("ReadAll for %q: no error", tc)
				return
			}
			t.Logf("%q: error %s", tc, err)
		})
	}
}
