// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package maintner

import (
	"context"
	"fmt"
	"reflect"
	"testing"
)

func TestSumSegSize(t *testing.T) {
	tests := []struct {
		in   []fileSeg
		want int64
	}{
		{
			in:   []fileSeg{fileSeg{size: 1}},
			want: 1,
		},
		{
			in:   []fileSeg{fileSeg{size: 1}, fileSeg{size: 100}},
			want: 101,
		},
		{
			in:   nil,
			want: 0,
		},
	}
	for i, tt := range tests {
		got := sumSegSize(tt.in)
		if got != tt.want {
			t.Errorf("%d. sumSegSize = %v; want %v", i, got, tt.want)
		}
	}
}

func TestSumCommonPrefixSize(t *testing.T) {
	tests := []struct {
		a, b   []fileSeg
		summer func(file string, n int64) string
		want   int64
	}{
		{
			a:    []fileSeg{fileSeg{size: 1, sha224: "abab"}},
			b:    []fileSeg{fileSeg{size: 1, sha224: "abab"}},
			want: 1,
		},
		{
			a:    []fileSeg{fileSeg{size: 1, sha224: "abab"}},
			b:    []fileSeg{fileSeg{size: 1, sha224: "eeee"}},
			want: 0,
		},
		{
			a: []fileSeg{
				fileSeg{size: 100, sha224: "abab"},
				fileSeg{size: 100, sha224: "abab", file: "a.mutlog"},
			},
			b: []fileSeg{
				fileSeg{size: 100, sha224: "abab"},
				fileSeg{size: 50, sha224: "cccc"},
			},
			summer: func(file string, n int64) string {
				if file == "a.mutlog" && n == 50 {
					return "cccc"
				}
				return "xxx"
			},
			want: 150,
		},
		{
			a: []fileSeg{
				fileSeg{size: 100, sha224: "abab"},
				fileSeg{size: 50, sha224: "cccc"},
			},
			b: []fileSeg{
				fileSeg{size: 100, sha224: "abab"},
				fileSeg{size: 100, sha224: "abab", file: "b.mutlog"},
			},
			summer: func(file string, n int64) string {
				if file == "b.mutlog" && n == 50 {
					return "cccc"
				}
				return "xxx"
			},
			want: 150,
		},
	}
	for i, tt := range tests {
		summer := tt.summer
		if summer == nil {
			summer = func(file string, n int64) string {
				t.Errorf("%d. unexpected call to prefix summer for file=%q, n=%v", i, file, n)
				return ""
			}
		}
		ns := &netMutSource{
			testHookFilePrefixSum224: summer,
		}
		got := ns.sumCommonPrefixSize(tt.a, tt.b)
		if got != tt.want {
			t.Errorf("%d. sumCommonPrefixSize = %v; want %v", i, got, tt.want)
		}
	}
}

func TestTrimLeadingSegBytes(t *testing.T) {
	tests := []struct {
		in   []fileSeg
		trim int64
		want []fileSeg
	}{
		{
			in:   []fileSeg{fileSeg{size: 100}, fileSeg{size: 50}},
			trim: 0,
			want: []fileSeg{fileSeg{size: 100}, fileSeg{size: 50}},
		},
		{
			in:   []fileSeg{fileSeg{size: 100}, fileSeg{size: 50}},
			trim: 150,
			want: nil,
		},
		{
			in:   []fileSeg{fileSeg{size: 100}, fileSeg{size: 50}},
			trim: 100,
			want: []fileSeg{fileSeg{size: 50}},
		},
		{
			in:   []fileSeg{fileSeg{size: 100}, fileSeg{size: 50}},
			trim: 25,
			want: []fileSeg{fileSeg{size: 100, skip: 25}, fileSeg{size: 50}},
		},
	}
	for i, tt := range tests {
		copyIn := append([]fileSeg(nil), tt.in...)
		got := trimLeadingSegBytes(tt.in, tt.trim)
		if !reflect.DeepEqual(tt.in, copyIn) {
			t.Fatalf("%d. trimLeadingSegBytes modified its input", i)
		}
		if !reflect.DeepEqual(got, tt.want) {
			t.Fatalf("%d. trim = %+v; want %+v", i, got, tt.want)
		}
	}
}

func TestGetNewSegments(t *testing.T) {
	type testCase struct {
		name       string
		lastSegs   []fileSeg
		serverSegs [][]LogSegmentJSON

		// prefixSum is the prefix sum to use if called.
		// If empty, prefixSum calls are errors.
		prefixSum string

		want      []fileSeg
		wantSplit bool
	}
	tests := []testCase{
		{
			name: "first_download",
			serverSegs: [][]LogSegmentJSON{
				[]LogSegmentJSON{
					{Number: 1, Size: 100, SHA224: "abc"},
					{Number: 2, Size: 200, SHA224: "def"},
				},
			},
			want: []fileSeg{
				{seg: 1, size: 100, sha224: "abc", file: "/fake/0001.mutlog"},
				{seg: 2, size: 200, sha224: "def", file: "/fake/0002.mutlog"},
			},
		},
		{
			name: "incremental_download_growseg", // from first_download, segment 2 grows a bit
			lastSegs: []fileSeg{
				{seg: 1, size: 100, sha224: "abc", file: "/fake/0001.mutlog"},
				{seg: 2, size: 200, sha224: "def", file: "/fake/0002.mutlog"},
			},
			prefixSum: "def",
			serverSegs: [][]LogSegmentJSON{
				[]LogSegmentJSON{
					{Number: 1, Size: 100, SHA224: "abc"},
					{Number: 2, Size: 205, SHA224: "defdef"},
				},
			},
			want: []fileSeg{
				{seg: 2, size: 205, sha224: "defdef", skip: 200, file: "/fake/0002.mutlog"},
			},
		},
		{
			name: "incremental_download_growseg_and_newseg", // from first_download, segment 2 grows, and segment 3 appears.
			lastSegs: []fileSeg{
				{seg: 1, size: 100, sha224: "abc", file: "/fake/0001.mutlog"},
				{seg: 2, size: 200, sha224: "def", file: "/fake/0002.mutlog"},
			},
			prefixSum: "def",
			serverSegs: [][]LogSegmentJSON{
				[]LogSegmentJSON{
					{Number: 1, Size: 100, SHA224: "abc"},
					{Number: 2, Size: 250, SHA224: "defdef"},
					{Number: 3, Size: 300, SHA224: "fff"},
				},
			},
			want: []fileSeg{
				{seg: 2, size: 250, sha224: "defdef", skip: 200, file: "/fake/0002.mutlog"},
				{seg: 3, size: 300, sha224: "fff", skip: 0, file: "/fake/0003.mutlog"},
			},
		},
		{
			name: "incremental_download_newseg", // from first_download, segment 3 appears.
			lastSegs: []fileSeg{
				{seg: 1, size: 100, sha224: "abc", file: "/fake/0001.mutlog"},
				{seg: 2, size: 200, sha224: "def", file: "/fake/0002.mutlog"},
			},
			serverSegs: [][]LogSegmentJSON{
				[]LogSegmentJSON{
					{Number: 1, Size: 100, SHA224: "abc"},
					{Number: 2, Size: 200, SHA224: "def"},
					{Number: 3, Size: 300, SHA224: "fff"},
				},
			},
			want: []fileSeg{
				{seg: 3, size: 300, sha224: "fff", skip: 0, file: "/fake/0003.mutlog"},
			},
		},
		{
			name: "incremental_with_sleep",
			lastSegs: []fileSeg{
				{seg: 1, size: 101, sha224: "abc", file: "/fake/0001.mutlog"},
			},
			serverSegs: [][]LogSegmentJSON{
				[]LogSegmentJSON{
					{Number: 1, Size: 101, SHA224: "abc"},
				},
				[]LogSegmentJSON{
					{Number: 1, Size: 101, SHA224: "abc"},
					{Number: 2, Size: 102, SHA224: "def"},
				},
			},
			want: []fileSeg{
				{seg: 2, size: 102, sha224: "def", skip: 0, file: "/fake/0002.mutlog"},
			},
		},
		{
			name: "split_error_diff_first_seg_same_size",
			lastSegs: []fileSeg{
				{seg: 1, size: 101, sha224: "abc", file: "/fake/0001.mutlog"},
			},
			serverSegs: [][]LogSegmentJSON{
				[]LogSegmentJSON{
					{Number: 1, Size: 101, SHA224: "def"},
				},
			},
			wantSplit: true,
		},
		{
			name: "split_error_diff_first_seg_and_longer",
			lastSegs: []fileSeg{
				{seg: 1, size: 101, sha224: "abc", file: "/fake/0001.mutlog"},
			},
			serverSegs: [][]LogSegmentJSON{
				[]LogSegmentJSON{
					{Number: 1, Size: 102, SHA224: "def"},
				},
			},
			prefixSum: "ffffffffff", // no match
			wantSplit: true,
		},
		{
			name: "split_error_diff_first_seg_and_shorter",
			lastSegs: []fileSeg{
				{seg: 1, size: 101, sha224: "abc", file: "/fake/0001.mutlog"},
			},
			serverSegs: [][]LogSegmentJSON{
				[]LogSegmentJSON{
					{Number: 1, Size: 50, SHA224: "def"},
				},
			},
			prefixSum: "ffffffffff", // no match
			wantSplit: true,
		},
		{
			name: "split_error_diff_final_seg",
			lastSegs: []fileSeg{
				{seg: 1, size: 100, sha224: "abc", file: "/fake/0001.mutlog"},
				{seg: 2, size: 2, sha224: "def", file: "/fake/0002.mutlog"},
			},
			serverSegs: [][]LogSegmentJSON{
				[]LogSegmentJSON{
					{Number: 1, Size: 100, SHA224: "abc"},
					{Number: 2, Size: 4, SHA224: "fff"},
				},
			},
			prefixSum: "not_def",
			wantSplit: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			serverSegCalls := 0
			waits := 0
			ns := &netMutSource{
				last: tt.lastSegs,
				testHookGetServerSegments: func(_ context.Context, waitSizeNot int64) (segs []LogSegmentJSON, err error) {
					serverSegCalls++
					if serverSegCalls > 10 {
						t.Fatalf("infinite loop calling getServerSegments? num wait calls = %v", waits)
					}
					if len(tt.serverSegs) == 0 {
						return nil, nil
					}
					segs = tt.serverSegs[0]
					if len(tt.serverSegs) > 1 {
						tt.serverSegs = tt.serverSegs[1:]
					}
					return segs, nil
				},
				testHookSyncSeg: func(_ context.Context, seg LogSegmentJSON) (fileSeg, []byte, error) {
					return fileSeg{
						seg:    seg.Number,
						size:   seg.Size,
						sha224: seg.SHA224,
						file:   fmt.Sprintf("/fake/%04d.mutlog", seg.Number),
					}, nil, nil
				},
				testHookFilePrefixSum224: func(file string, n int64) string {
					if tt.prefixSum != "" {
						return tt.prefixSum
					}
					t.Errorf("unexpected call to filePrefixSum224(%q, %d)", file, n)
					return "XXXX"
				},
			}
			got, err := ns.getNewSegments(context.Background())
			if tt.wantSplit && err == ErrSplit {
				// Success.
				return
			}
			if tt.wantSplit {
				t.Fatalf("wanted ErrSplit; got %+v, %v", got, err)
			}
			if err != nil {
				t.Fatal(err)
			}
			if !reflect.DeepEqual(got, tt.want) {
				t.Errorf("mismatch\n got: %+v\nwant: %+v\n", got, tt.want)
			}
		})
	}
}
