// Copyright 2015, Joe Tsai. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE.md file.

package xflate

import (
	"bytes"
	"compress/flate"
	"io/ioutil"
	"testing"

	"github.com/dsnet/compress/internal/testutil"
)

func TestWriter(t *testing.T) {
	br := bytes.Repeat
	dh := testutil.MustDecodeHex

	vectors := []struct {
		desc   string        // Test description
		conf   *WriterConfig // Input WriterConfig
		input  []interface{} // Test input tokens (either flush mode or input string)
		output []byte        // Expected output string
	}{{
		desc:  "empty stream",
		input: []interface{}{},
		output: dh("" +
			"0d008705000048c82a51e8ff37dbf1", // Footer
		),
	}, {
		desc:  "empty stream with empty chunk",
		input: []interface{}{FlushSync},
		output: dh("" +
			"000000ffff000000ffff" + // Chunk0
			"34c086050020916cb2a50bd20369da192deaff3bda05f8" + // Index0
			"1dc08605002021ab44219b4aff7fd6de3bf8", // Footer
		),
	}, {
		desc:  "empty stream with empty index",
		input: []interface{}{FlushIndex},
		output: dh("" +
			"04c086050020191d53a1a508c9e8ff5bda7bf8" + // Index0
			"15c08605002021ab44219ba2ff2f6bef5df8", // Footer
		),
	}, {
		desc:  "empty stream with multiple empty chunks",
		input: []interface{}{FlushFull, FlushFull, FlushFull},
		output: dh("" +
			"000000ffff" + // Chunk0
			"000000ffff" + // Chunk1
			"000000ffff" + // Chunk2
			"148086058044655366e3817441ba205d504a83348c445ddcde7b6ffc" + // Index0
			"15c08605002021ab44a103aaff2f6bef5df8", // Footer
		),
	}, {
		desc:  "empty stream with multiple empty indexes",
		input: []interface{}{FlushIndex, FlushIndex, FlushIndex},
		output: dh("" +
			"04c086050020191d53a1a508c9e8ff5bda7bf8" + // Index0
			"3cc08605002019293a24a55464a585faff9bf600f8" + // Index1
			"04c08605002019493a2494d050560afd7f4c7bfb" + // Index2
			"25008705000048c82a51e880f4ff834df0", // Footer
		),
	}, {
		desc:  "3k zeros, 1KiB chunks",
		conf:  &WriterConfig{ChunkSize: 1 << 10},
		input: []interface{}{br([]byte{0}, 3000)},
		output: dh("" +
			"621805a360148c5800000000ffff" + // Chunk0
			"621805a360148c5800000000ffff" + // Chunk1
			"621805a360140c3900000000ffff" + // Chunk2
			"1c8086058044642b3bc9aa3464540784acea809055d99586dd5492446555a7b607fc" + // Index0
			"0d008705000048c82a51c81ea1ff0f6cf2", // Footer
		),
	}, {
		desc: "quick brown fox - manual chunking/indexing",
		input: []interface{}{
			"the quick", FlushSync, " brown fox", FlushFull, FlushFull, " jumped over the", FlushIndex, " lazy dog",
		},
		output: dh("" +
			"2ac94855282ccd4cce06000000ffff52482aca2fcf5348cbaf00000000ffff" + // Chunk0
			"000000ffff" + // Chunk1
			"52c82acd2d484d51c82f4b2d5228c94805000000ffff" + // Chunk2
			"2480860580446553762a0ad14211d207253b234546a1528ad4d3edbd0bfc" + // Index0
			"52c849acaa5448c94f07000000ffff" + // Chunk3
			"2c8086058044a281ec8611190d23b21221ca0851fdafbdf7de05fc" + // Index1
			"1dc08605002021ab44219b52ff7fd6de3bf8", // Footer
		),
	}, {
		desc:  "quick brown fox - automatic chunking/indexing",
		conf:  &WriterConfig{ChunkSize: 4, IndexSize: 3},
		input: []interface{}{"the quick brown fox jumped over the lazy dog"},
		output: dh("" +
			"2ac9485500000000ffff" + // Chunk0
			"2a2ccd4c06000000ffff" + // Chunk1
			"ca56482a02000000ffff" + // Chunk2
			"2c8086058044655376c32a2b9999c9cc4c665691d04ea5a474747bef01fc" + // Index0
			"ca2fcf5300000000ffff" + // Chunk3
			"4acbaf5000000000ffff" + // Chunk4
			"ca2acd2d00000000ffff" + // Chunk5
			"04808605804445036537acb2929999cccc6466cb48112a45a193db7beffc" + // Index1
			"4a4d51c807000000ffff" + // Chunk6
			"2a4b2d5200000000ffff" + // Chunk7
			"2ac9485500000000ffff" + // Chunk8
			"04808605804445036537acb2929999cccc6466cb48112a45a193db7beffc" + // Index2
			"ca49acaa04000000ffff" + // Chunk9
			"5248c94f07000000ffff" + // Chunk10
			"148086058084a261644b665632339399d9425629a44877b7f7de3bfc" + // Index3
			"15c08605002021ab44a103aaff2f6bef5df8", // Footer
		),
	}}

	for i, v := range vectors {
		// Encode the test input.
		var b, bb bytes.Buffer
		xw, err := NewWriter(&b, v.conf)
		if err != nil {
			t.Errorf("test %d (%s), unexpected error: NewWriter() = %v", i, v.desc, err)
		}
		for _, tok := range v.input {
			switch tok := tok.(type) {
			case string:
				bb.WriteString(tok)
				if _, err := xw.Write([]byte(tok)); err != nil {
					t.Errorf("test %d (%s), unexpected error: Write() = %v", i, v.desc, err)
				}
			case []byte:
				bb.Write(tok)
				if _, err := xw.Write(tok); err != nil {
					t.Errorf("test %d (%s), unexpected error: Write() = %v", i, v.desc, err)
				}
			case FlushMode:
				if err := xw.Flush(tok); err != nil {
					t.Errorf("test %d (%s), unexpected error: Flush() = %v", i, v.desc, err)
				}
			default:
				t.Fatalf("test %d (%s), unknown token: %v", i, v.desc, tok)
			}
		}
		if err := xw.Close(); err != nil {
			t.Errorf("test %d (%s), unexpected error: Close() = %v", i, v.desc, err)
		}
		if got, want, ok := testutil.BytesCompare(b.Bytes(), v.output); !ok {
			t.Errorf("test %d (%s), mismatching bytes:\ngot  %s\nwant %s", i, v.desc, got, want)
		}
		if xw.OutputOffset != int64(b.Len()) {
			t.Errorf("test %d (%s), output offset mismatch: got %d, want %d", i, v.desc, xw.OutputOffset, b.Len())
		}
		if xw.InputOffset != int64(bb.Len()) {
			t.Errorf("test %d (%s), input offset mismatch: got %d, want %d", i, v.desc, xw.InputOffset, bb.Len())
		}

		// Verify that the output stream is DEFLATE compatible.
		rd := bytes.NewReader(b.Bytes())
		fr := flate.NewReader(rd)
		buf, err := ioutil.ReadAll(fr)
		if err != nil {
			t.Errorf("test %d (%s), unexpected error: ReadAll() = %v", i, v.desc, err)
		}
		if got, want, ok := testutil.BytesCompare(buf, bb.Bytes()); !ok {
			t.Errorf("test %d (%s), mismatching bytes:\ngot  %s\nwant %s", i, v.desc, got, want)
		}
		if rd.Len() > 0 {
			t.Errorf("test %d (%s), not all bytes consumed: %d > 0", i, v.desc, rd.Len())
		}
	}
}

func TestWriterReset(t *testing.T) {
	// Test bad Writer config.
	if _, err := NewWriter(ioutil.Discard, &WriterConfig{Level: -431}); err == nil {
		t.Fatalf("unexpected success: NewWriter()")
	}

	// Test Writer for idempotent Close.
	xw := new(Writer)
	xw.Reset(ioutil.Discard)
	if _, err := xw.Write([]byte("hello, world!")); err != nil {
		t.Fatalf("unexpected error: Write() = %v", err)
	}
	if err := xw.Close(); err != nil {
		t.Fatalf("unexpected error: Close() = %v", err)
	}
	if err := xw.Close(); err != nil {
		t.Fatalf("unexpected error: Close() = %v", err)
	}
	if _, err := xw.Write([]byte("hello, world!")); err != errClosed {
		t.Fatalf("mismatching error: Write() = %v, want %v", err, errClosed)
	}
}

// BenchmarkWriter benchmarks the overhead of the XFLATE format over DEFLATE.
// Thus, it intentionally uses a very small chunk size with no compression.
func BenchmarkWriter(b *testing.B) {
	twain := testutil.MustLoadFile("../testdata/twain.txt")
	bb := bytes.NewBuffer(make([]byte, 0, 2*len(twain)))
	xw, _ := NewWriter(nil, &WriterConfig{Level: NoCompression, ChunkSize: 1 << 10})

	b.ReportAllocs()
	b.SetBytes(int64(len(twain)))
	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		bb.Reset()
		xw.Reset(bb)
		if _, err := xw.Write(twain); err != nil {
			b.Fatalf("unexpected error: Write() = %v", err)
		}
		if err := xw.Close(); err != nil {
			b.Fatalf("unexpected error: Close() = %v", err)
		}
	}
}
