// Licensed to Apache Software Foundation (ASF) under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Apache Software Foundation (ASF) licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

package stream

import (
	"context"
	"errors"
	"testing"
	"time"

	"github.com/google/go-cmp/cmp"
	"github.com/google/go-cmp/cmp/cmpopts"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/apache/skywalking-banyandb/api/common"
	"github.com/apache/skywalking-banyandb/banyand/protector"
	"github.com/apache/skywalking-banyandb/pkg/convert"
	"github.com/apache/skywalking-banyandb/pkg/fs"
	"github.com/apache/skywalking-banyandb/pkg/logger"
	pbv1 "github.com/apache/skywalking-banyandb/pkg/pb/v1"
	"github.com/apache/skywalking-banyandb/pkg/run"
	"github.com/apache/skywalking-banyandb/pkg/test"
	"github.com/apache/skywalking-banyandb/pkg/timestamp"
	"github.com/apache/skywalking-banyandb/pkg/watcher"
)

func Test_tsTable_mustAddElements(t *testing.T) {
	tests := []struct {
		name   string
		esList []*elements
		want   int
	}{
		{
			name: "Test with empty elements",
			esList: []*elements{
				{
					timestamps:  []int64{},
					elementIDs:  []uint64{},
					seriesIDs:   []common.SeriesID{},
					tagFamilies: make([][]tagValues, 0),
				},
			},
			want: 0,
		},
		{
			name: "Test with one item in elements",
			esList: []*elements{
				{
					timestamps: []int64{1},
					elementIDs: []uint64{0},
					seriesIDs:  []common.SeriesID{1},
					tagFamilies: [][]tagValues{
						{
							{
								tag: "arrTag", values: []*tagValue{
									{tag: "strArrTag", valueType: pbv1.ValueTypeStrArr, value: nil, valueArr: [][]byte{[]byte("value1"), []byte("value2")}},
									{tag: "intArrTag", valueType: pbv1.ValueTypeInt64Arr, value: nil, valueArr: [][]byte{convert.Int64ToBytes(25), convert.Int64ToBytes(30)}},
								},
							},
						},
					},
				},
			},
			want: 1,
		},
		{
			name: "Test with multiple calls to mustAddDataPoints",
			esList: []*elements{
				esTS1,
				esTS2,
			},
			want: 2,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tmpPath, _ := test.Space(require.New(t))
			index, _ := newElementIndex(context.TODO(), tmpPath, 0, nil)
			tst := &tsTable{
				index:         index,
				loopCloser:    run.NewCloser(2),
				introductions: make(chan *introduction),
			}
			flushCh := make(chan *flusherIntroduction)
			mergeCh := make(chan *mergerIntroduction)
			introducerWatcher := make(watcher.Channel, 1)
			go tst.introducerLoop(flushCh, mergeCh, introducerWatcher, 1)
			defer tst.Close()
			for _, es := range tt.esList {
				tst.mustAddElements(es)
				time.Sleep(100 * time.Millisecond)
			}
			s := tst.currentSnapshot()
			if s == nil {
				s = new(snapshot)
			}
			defer s.decRef()
			assert.Equal(t, tt.want, len(s.parts))
			var lastVersion uint64
			for _, pw := range s.parts {
				require.Greater(t, pw.ID(), uint64(0))
				if lastVersion == 0 {
					lastVersion = pw.ID()
				} else {
					require.Less(t, lastVersion, pw.ID())
				}
			}
		})
	}
}

func Test_tstIter(t *testing.T) {
	type testCtx struct {
		wantErr      error
		name         string
		esList       []*elements
		sids         []common.SeriesID
		want         []blockMetadata
		minTimestamp int64
		maxTimestamp int64
	}
	bma := generateBlockMetadataArray()
	defer releaseBlockMetadataArray(bma)

	verify := func(t *testing.T, tt testCtx, tst *tsTable) uint64 {
		defer tst.Close()
		s := tst.currentSnapshot()
		if s == nil {
			s = new(snapshot)
		}
		defer s.decRef()
		pp, n := s.getParts(nil, tt.minTimestamp, tt.maxTimestamp)
		require.Equal(t, len(s.parts), n)
		ti := &tstIter{}
		ti.init(bma, pp, tt.sids, tt.minTimestamp, tt.maxTimestamp, nil)
		var got []blockMetadata
		for ti.nextBlock() {
			if ti.piHeap[0].curBlock.seriesID == 0 {
				t.Errorf("Expected curBlock to be initialized, but it was nil")
			}
			var bm blockMetadata
			bm.copyFrom(ti.piHeap[0].curBlock)
			got = append(got, bm)
		}

		if !errors.Is(ti.Error(), tt.wantErr) {
			t.Errorf("Unexpected error: got %v, want %v", ti.err, tt.wantErr)
		}

		if diff := cmp.Diff(got, tt.want,
			cmpopts.IgnoreFields(blockMetadata{}, "timestamps"),
			cmpopts.IgnoreFields(blockMetadata{}, "elementIDs"),
			cmpopts.IgnoreFields(blockMetadata{}, "tagFamilies"),
			cmp.AllowUnexported(blockMetadata{}),
		); diff != "" {
			t.Errorf("Unexpected blockMetadata (-got +want):\n%s", diff)
		}
		return s.epoch
	}

	t.Run("memory snapshot", func(t *testing.T) {
		tests := []testCtx{
			{
				name:         "Test with no data points",
				esList:       []*elements{},
				sids:         []common.SeriesID{1, 2, 3},
				minTimestamp: 1,
				maxTimestamp: 1,
			},
			{
				name:         "Test with single part",
				esList:       []*elements{esTS1},
				sids:         []common.SeriesID{1, 2, 3},
				minTimestamp: 1,
				maxTimestamp: 1,
				want: []blockMetadata{
					{seriesID: 1, count: 1, uncompressedSizeBytes: 859},
					{seriesID: 2, count: 1, uncompressedSizeBytes: 47},
					{seriesID: 3, count: 1, uncompressedSizeBytes: 16},
				},
			},
			{
				name:         "Test with multiple parts with different ts",
				esList:       []*elements{esTS1, esTS2},
				sids:         []common.SeriesID{1, 2, 3},
				minTimestamp: 1,
				maxTimestamp: 2,
				want: []blockMetadata{
					{seriesID: 1, count: 1, uncompressedSizeBytes: 859},
					{seriesID: 1, count: 1, uncompressedSizeBytes: 859},
					{seriesID: 2, count: 1, uncompressedSizeBytes: 47},
					{seriesID: 2, count: 1, uncompressedSizeBytes: 47},
					{seriesID: 3, count: 1, uncompressedSizeBytes: 16},
					{seriesID: 3, count: 1, uncompressedSizeBytes: 16},
				},
			},
			{
				name:         "Test with multiple parts with same ts",
				esList:       []*elements{esTS1, esTS1},
				sids:         []common.SeriesID{1, 2, 3},
				minTimestamp: 1,
				maxTimestamp: 2,
				want: []blockMetadata{
					{seriesID: 1, count: 1, uncompressedSizeBytes: 859},
					{seriesID: 1, count: 1, uncompressedSizeBytes: 859},
					{seriesID: 2, count: 1, uncompressedSizeBytes: 47},
					{seriesID: 2, count: 1, uncompressedSizeBytes: 47},
					{seriesID: 3, count: 1, uncompressedSizeBytes: 16},
					{seriesID: 3, count: 1, uncompressedSizeBytes: 16},
				},
			},
		}
		for _, tt := range tests {
			t.Run(tt.name, func(t *testing.T) {
				tmpPath, defFn := test.Space(require.New(t))
				index, _ := newElementIndex(context.TODO(), tmpPath, 0, nil)
				defer defFn()
				tst := &tsTable{
					index:         index,
					loopCloser:    run.NewCloser(2),
					introductions: make(chan *introduction),
					fileSystem:    fs.NewLocalFileSystem(),
					root:          tmpPath,
				}
				tst.gc.init(tst)
				flushCh := make(chan *flusherIntroduction)
				mergeCh := make(chan *mergerIntroduction)
				introducerWatcher := make(watcher.Channel, 1)
				go tst.introducerLoop(flushCh, mergeCh, introducerWatcher, 1)
				for _, es := range tt.esList {
					tst.mustAddElements(es)
					time.Sleep(100 * time.Millisecond)
				}
				verify(t, tt, tst)
			})
		}
	})
}

func Test_mustAddMemPart_closeNotifyReleasesMemPart(t *testing.T) {
	tmpPath, defFn := test.Space(require.New(t))
	defer defFn()
	tst, err := newTSTable(
		fs.NewLocalFileSystem(),
		tmpPath,
		common.Position{},
		logger.GetLogger("test"),
		timestamp.TimeRange{},
		option{
			flushTimeout: 0,
			protector:    protector.Nop{},
		},
		nil,
	)
	require.NoError(t, err)

	mp := generateMemPart()
	mp.mustInitFromElements(esTS1)
	originalCount := mp.partMetadata.TotalCount
	require.Greater(t, originalCount, uint64(0))

	tst.Close()
	tst.mustAddMemPart(mp)

	// Verify the memPart was released by checking the count is reset to 0
	// after mustAddMemPart returns (which handles the release via decRef)
	require.Equal(t, uint64(0), mp.partMetadata.TotalCount)
}

var esTS1 = &elements{
	seriesIDs:  []common.SeriesID{1, 2, 3},
	timestamps: []int64{1, 1, 1},
	elementIDs: []uint64{11, 21, 31},
	tagFamilies: [][]tagValues{
		{
			{
				tag: "arrTag", values: []*tagValue{
					{tag: "strArrTag", valueType: pbv1.ValueTypeStrArr, value: nil, valueArr: [][]byte{[]byte("value1"), []byte("value2")}},
					{tag: "intArrTag", valueType: pbv1.ValueTypeInt64Arr, value: nil, valueArr: [][]byte{convert.Int64ToBytes(25), convert.Int64ToBytes(30)}},
				},
			},
			{
				tag: "binaryTag", values: []*tagValue{
					{tag: "binaryTag", valueType: pbv1.ValueTypeBinaryData, value: longText, valueArr: nil},
				},
			},
			{
				tag: "singleTag", values: []*tagValue{
					{tag: "strTag", valueType: pbv1.ValueTypeStr, value: []byte("value1"), valueArr: nil},
					{tag: "intTag", valueType: pbv1.ValueTypeInt64, value: convert.Int64ToBytes(10), valueArr: nil},
				},
			},
		},
		{
			{
				tag: "singleTag", values: []*tagValue{
					{tag: "strTag1", valueType: pbv1.ValueTypeStr, value: []byte("tag1"), valueArr: nil},
					{tag: "strTag2", valueType: pbv1.ValueTypeStr, value: []byte("tag2"), valueArr: nil},
				},
			},
		},
		{}, // empty tagFamilies for seriesID 3
	},
}

var esTS2 = &elements{
	seriesIDs:  []common.SeriesID{1, 2, 3},
	timestamps: []int64{2, 2, 2},
	elementIDs: []uint64{12, 22, 32},
	tagFamilies: [][]tagValues{
		{
			{
				tag: "arrTag", values: []*tagValue{
					{tag: "strArrTag", valueType: pbv1.ValueTypeStrArr, value: nil, valueArr: [][]byte{[]byte("value5"), []byte("value6")}},
					{tag: "intArrTag", valueType: pbv1.ValueTypeInt64Arr, value: nil, valueArr: [][]byte{convert.Int64ToBytes(35), convert.Int64ToBytes(40)}},
				},
			},
			{
				tag: "binaryTag", values: []*tagValue{
					{tag: "binaryTag", valueType: pbv1.ValueTypeBinaryData, value: longText, valueArr: nil},
				},
			},
			{
				tag: "singleTag", values: []*tagValue{
					{tag: "strTag", valueType: pbv1.ValueTypeStr, value: []byte("value3"), valueArr: nil},
					{tag: "intTag", valueType: pbv1.ValueTypeInt64, value: convert.Int64ToBytes(30), valueArr: nil},
				},
			},
		},
		{
			{
				tag: "singleTag", values: []*tagValue{
					{tag: "strTag1", valueType: pbv1.ValueTypeStr, value: []byte("tag3"), valueArr: nil},
					{tag: "strTag2", valueType: pbv1.ValueTypeStr, value: []byte("tag4"), valueArr: nil},
				},
			},
		},
		{}, // empty tagFamilies for seriesID 6
	},
}

func generateHugeElements(startTimestamp, endTimestamp, timestamp int64) *elements {
	elements := &elements{
		seriesIDs:   []common.SeriesID{},
		timestamps:  []int64{},
		elementIDs:  []uint64{},
		tagFamilies: [][]tagValues{},
	}
	for i := startTimestamp; i <= endTimestamp; i++ {
		elements.seriesIDs = append(elements.seriesIDs, 1)
		elements.timestamps = append(elements.timestamps, i)
		elements.elementIDs = append(elements.elementIDs, 10+uint64(i))
		elements.tagFamilies = append(elements.tagFamilies, []tagValues{
			{
				tag: "arrTag", values: []*tagValue{
					{tag: "strArrTag", valueType: pbv1.ValueTypeStrArr, value: nil, valueArr: [][]byte{[]byte("value5"), []byte("value6")}},
					{tag: "intArrTag", valueType: pbv1.ValueTypeInt64Arr, value: nil, valueArr: [][]byte{convert.Int64ToBytes(35), convert.Int64ToBytes(40)}},
				},
			},
			{
				tag: "binaryTag", values: []*tagValue{
					{tag: "binaryTag", valueType: pbv1.ValueTypeBinaryData, value: longText, valueArr: nil},
				},
			},
			{
				tag: "singleTag", values: []*tagValue{
					{tag: "strTag", valueType: pbv1.ValueTypeStr, value: []byte("value3"), valueArr: nil},
					{tag: "intTag", valueType: pbv1.ValueTypeInt64, value: convert.Int64ToBytes(30), valueArr: nil},
				},
			},
		})
	}
	elements.seriesIDs = append(elements.seriesIDs, []common.SeriesID{2, 3}...)
	elements.timestamps = append(elements.timestamps, []int64{timestamp, timestamp}...)
	elements.elementIDs = append(elements.elementIDs, []uint64{20 + uint64(timestamp), 30 + uint64(timestamp)}...)
	elements.tagFamilies = append(elements.tagFamilies, [][]tagValues{{
		{
			tag: "singleTag", values: []*tagValue{
				{tag: "strTag1", valueType: pbv1.ValueTypeStr, value: []byte("tag3"), valueArr: nil},
				{tag: "strTag2", valueType: pbv1.ValueTypeStr, value: []byte("tag4"), valueArr: nil},
			},
		},
	}, {}}...)
	return elements
}
