// Copyright 2018 PingCAP, Inc.
//
// Licensed 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 memory

import (
	"errors"
	"math/rand"
	"runtime"
	"runtime/debug"
	"strconv"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/pingcap/tidb/pkg/errno"
	"github.com/pingcap/tidb/pkg/parser/terror"
	"github.com/stretchr/testify/require"
)

func TestSetLabel(t *testing.T) {
	tracker := NewTracker(1, -1)
	require.Equal(t, 1, tracker.label)
	require.Equal(t, int64(0), tracker.BytesConsumed())
	require.Equal(t, int64(-1), tracker.GetBytesLimit())
	require.Nil(t, tracker.getParent())
	require.Equal(t, 0, len(tracker.mu.children))
	tracker.SetLabel(2)
	require.Equal(t, 2, tracker.label)
	require.Equal(t, int64(0), tracker.BytesConsumed())
	require.Equal(t, int64(-1), tracker.GetBytesLimit())
	require.Nil(t, tracker.getParent())
	require.Equal(t, 0, len(tracker.mu.children))
}

func TestSetLabel2(t *testing.T) {
	tracker := NewTracker(1, -1)
	tracker2 := NewTracker(2, -1)
	tracker2.AttachTo(tracker)
	tracker2.Consume(10)
	require.Equal(t, tracker.BytesConsumed(), int64(10))
	tracker2.SetLabel(10)
	require.Equal(t, tracker.BytesConsumed(), int64(10))
	tracker2.Detach()
	require.Equal(t, tracker.BytesConsumed(), int64(0))
}

func TestConsume(t *testing.T) {
	tracker := NewTracker(1, -1)
	require.Equal(t, int64(0), tracker.BytesConsumed())

	tracker.Consume(100)
	require.Equal(t, int64(100), tracker.BytesConsumed())

	waitGroup := sync.WaitGroup{}
	waitGroup.Add(10)
	for range 10 {
		go func() {
			defer waitGroup.Done()
			tracker.Consume(10)
		}()
	}
	waitGroup.Add(10)
	for range 10 {
		go func() {
			defer waitGroup.Done()
			tracker.Consume(-10)
		}()
	}

	waitGroup.Wait()
	require.Equal(t, int64(100), tracker.BytesConsumed())
}

func TestRelease(t *testing.T) {
	debug.SetGCPercent(-1)
	defer debug.SetGCPercent(100)
	parentTracker := NewGlobalTracker(LabelForGlobalAnalyzeMemory, -1)
	tracker := NewTracker(1, -1)
	tracker.AttachToGlobalTracker(parentTracker)
	require.Equal(t, int64(0), tracker.BytesConsumed())
	EnableGCAwareMemoryTrack.Store(false)
	tracker.Consume(100)
	require.Equal(t, int64(100), tracker.BytesConsumed())
	require.Equal(t, int64(100), parentTracker.BytesConsumed())
	tracker.Release(100)
	require.Equal(t, int64(0), tracker.BytesConsumed())
	require.Equal(t, int64(0), parentTracker.BytesConsumed())
	require.Equal(t, int64(0), tracker.BytesReleased())
	require.Equal(t, int64(0), parentTracker.BytesReleased())

	EnableGCAwareMemoryTrack.Store(true)
	tracker.Consume(100)
	require.Equal(t, int64(100), tracker.BytesConsumed())
	require.Equal(t, int64(100), parentTracker.BytesConsumed())
	tracker.Release(100)
	require.Equal(t, int64(0), tracker.BytesConsumed())
	require.Equal(t, int64(0), parentTracker.BytesConsumed())
	require.Equal(t, int64(0), tracker.BytesReleased())
	require.Equal(t, int64(100), parentTracker.BytesReleased())
	// finalizer func is called async, need to wait for it to be called
	for {
		runtime.GC()
		if parentTracker.BytesReleased() == 0 {
			break
		}
		time.Sleep(time.Millisecond * 5)
	}
	waitGroup := sync.WaitGroup{}
	waitGroup.Add(10)
	for range 10 {
		go func() {
			defer waitGroup.Done()
			tracker.Consume(10)
		}()
	}
	waitGroup.Add(10)
	for range 10 {
		go func() {
			defer waitGroup.Done()
			tracker.Release(10)
		}()
	}
	waitGroup.Wait()
	// finalizer func is called async, need to wait for it to be called
	for {
		runtime.GC()
		if parentTracker.BytesReleased() == 0 {
			break
		}
		time.Sleep(time.Millisecond * 5)
	}
	require.Equal(t, int64(0), tracker.BytesConsumed())
	require.Equal(t, int64(0), parentTracker.BytesConsumed())
	require.Equal(t, int64(0), tracker.BytesReleased())
}

func TestBufferedConsumeAndRelease(t *testing.T) {
	debug.SetGCPercent(-1)
	defer debug.SetGCPercent(100)
	parentTracker := NewGlobalTracker(LabelForGlobalAnalyzeMemory, -1)
	tracker := NewTracker(1, -1)
	tracker.AttachToGlobalTracker(parentTracker)
	require.Equal(t, int64(0), tracker.BytesConsumed())
	EnableGCAwareMemoryTrack.Store(true)
	bufferedMemSize := int64(0)
	tracker.BufferedConsume(&bufferedMemSize, int64(TrackMemWhenExceeds)/2)
	require.Equal(t, int64(0), tracker.BytesConsumed())
	tracker.BufferedConsume(&bufferedMemSize, int64(TrackMemWhenExceeds)/2)
	require.Equal(t, int64(TrackMemWhenExceeds), tracker.BytesConsumed())
	bufferedReleaseSize := int64(0)
	tracker.BufferedRelease(&bufferedReleaseSize, int64(TrackMemWhenExceeds)/2)
	require.Equal(t, int64(TrackMemWhenExceeds), parentTracker.BytesConsumed())
	require.Equal(t, int64(0), parentTracker.BytesReleased())
	tracker.BufferedRelease(&bufferedReleaseSize, int64(TrackMemWhenExceeds)/2)
	require.Equal(t, int64(0), parentTracker.BytesConsumed())
	require.Equal(t, int64(TrackMemWhenExceeds), parentTracker.BytesReleased())
	// finalizer func is called async, need to wait for it to be called
	for {
		runtime.GC()
		if parentTracker.BytesReleased() == 0 {
			break
		}
		time.Sleep(time.Millisecond * 5)
	}
}

func TestOOMAction(t *testing.T) {
	tracker := NewTracker(1, 100)
	// make sure no panic here.
	tracker.Consume(10000)

	tracker = NewTracker(1, 100)
	action := &mockAction{}
	tracker.SetActionOnExceed(action)

	require.False(t, action.called)
	tracker.Consume(10000)
	require.True(t, action.called)

	// test fallback
	action1 := &mockAction{}
	action2 := &mockAction{}
	tracker.SetActionOnExceed(action1)
	tracker.FallbackOldAndSetNewAction(action2)
	require.False(t, action1.called)
	require.False(t, action2.called)
	tracker.Consume(10000)
	require.True(t, action2.called)
	require.False(t, action1.called)
	tracker.Consume(10000)
	require.True(t, action1.called)
	require.True(t, action2.called)

	// test softLimit
	tracker = NewTracker(1, 100)
	action1 = &mockAction{}
	action2 = &mockAction{}
	action3 := &mockAction{}
	tracker.SetActionOnExceed(action1)
	tracker.FallbackOldAndSetNewActionForSoftLimit(action2)
	tracker.FallbackOldAndSetNewActionForSoftLimit(action3)
	require.False(t, action3.called)
	require.False(t, action2.called)
	require.False(t, action1.called)
	tracker.Consume(80)
	require.True(t, action3.called)
	require.False(t, action2.called)
	require.False(t, action1.called)
	tracker.Consume(20)
	require.True(t, action3.called)
	require.True(t, action2.called) // SoftLimit fallback
	require.True(t, action1.called) // HardLimit

	// test fallback
	action1 = &mockAction{}
	action2 = &mockAction{}
	action3 = &mockAction{}
	action4 := &mockAction{}
	action5 := &mockAction{}
	tracker.SetActionOnExceed(action1)
	tracker.FallbackOldAndSetNewAction(action2)
	tracker.FallbackOldAndSetNewAction(action3)
	tracker.FallbackOldAndSetNewAction(action4)
	tracker.FallbackOldAndSetNewAction(action5)
	require.Equal(t, action5, tracker.actionMuForHardLimit.actionOnExceed)
	require.Equal(t, action4, tracker.actionMuForHardLimit.actionOnExceed.GetFallback())
	action4.SetFinished()
	require.Equal(t, action3, tracker.actionMuForHardLimit.actionOnExceed.GetFallback())
	action3.SetFinished()
	action2.SetFinished()
	require.Equal(t, action1, tracker.actionMuForHardLimit.actionOnExceed.GetFallback())
}

type mockAction struct {
	BaseOOMAction
	called   bool
	priority int64
}

func (a *mockAction) Action(t *Tracker) {
	if a.called && a.fallbackAction != nil {
		a.fallbackAction.Action(t)
		return
	}
	a.called = true
}

func (a *mockAction) GetPriority() int64 {
	return a.priority
}

func TestAttachTo(t *testing.T) {
	oldParent := NewTracker(1, -1)
	newParent := NewTracker(2, -1)
	child := NewTracker(3, -1)
	child.Consume(100)
	child.AttachTo(oldParent)
	require.Equal(t, int64(100), child.BytesConsumed())
	require.Equal(t, int64(100), oldParent.BytesConsumed())
	require.Equal(t, oldParent, child.getParent())
	require.Equal(t, 1, len(oldParent.mu.children))
	require.Equal(t, child, oldParent.mu.children[child.label][0])

	child.AttachTo(newParent)
	require.Equal(t, int64(100), child.BytesConsumed())
	require.Equal(t, int64(0), oldParent.BytesConsumed())
	require.Equal(t, int64(100), newParent.BytesConsumed())
	require.Equal(t, newParent, child.getParent())
	require.Equal(t, 1, len(newParent.mu.children))
	require.Equal(t, child, newParent.mu.children[child.label][0])
	require.Equal(t, 0, len(oldParent.mu.children))
}

func TestDetach(t *testing.T) {
	parent := NewTracker(1, -1)
	child := NewTracker(2, -1)
	child.Consume(100)
	child.AttachTo(parent)
	require.Equal(t, int64(100), child.BytesConsumed())
	require.Equal(t, int64(100), parent.BytesConsumed())
	require.Equal(t, 1, len(parent.mu.children))
	require.Equal(t, child, parent.mu.children[child.label][0])

	child.Detach()
	require.Equal(t, int64(100), child.BytesConsumed())
	require.Equal(t, int64(0), parent.BytesConsumed())
	require.Equal(t, 0, len(parent.mu.children))
	require.Nil(t, child.getParent())
}

func TestReplaceChild(t *testing.T) {
	oldChild := NewTracker(1, -1)
	oldChild.Consume(100)
	newChild := NewTracker(2, -1)
	newChild.Consume(500)
	parent := NewTracker(3, -1)

	oldChild.AttachTo(parent)
	require.Equal(t, int64(100), parent.BytesConsumed())

	parent.ReplaceChild(oldChild, newChild)
	require.Equal(t, int64(500), parent.BytesConsumed())
	require.Equal(t, 1, len(parent.mu.children))
	require.Equal(t, newChild, parent.mu.children[newChild.label][0])
	require.Equal(t, parent, newChild.getParent())
	require.Nil(t, oldChild.getParent())

	parent.ReplaceChild(oldChild, nil)
	require.Equal(t, int64(500), parent.BytesConsumed())
	require.Equal(t, 1, len(parent.mu.children))
	require.Equal(t, newChild, parent.mu.children[newChild.label][0])
	require.Equal(t, parent, newChild.getParent())
	require.Nil(t, oldChild.getParent())

	parent.ReplaceChild(newChild, nil)
	require.Equal(t, int64(0), parent.BytesConsumed())
	require.Equal(t, 0, len(parent.mu.children))
	require.Nil(t, newChild.getParent())
	require.Nil(t, oldChild.getParent())

	node1 := NewTracker(1, -1)
	node2 := NewTracker(2, -1)
	node3 := NewTracker(3, -1)
	node2.AttachTo(node1)
	node3.AttachTo(node2)
	node3.Consume(100)
	require.Equal(t, int64(100), node1.BytesConsumed())
	node2.ReplaceChild(node3, nil)
	require.Equal(t, int64(0), node2.BytesConsumed())
	require.Equal(t, int64(0), node1.BytesConsumed())
}

func TestToString(t *testing.T) {
	parent := NewTracker(1, -1)
	child1 := NewTracker(2, 1000)
	child2 := NewTracker(3, -1)
	child3 := NewTracker(4, -1)
	child4 := NewTracker(5, -1)

	child1.AttachTo(parent)
	child2.AttachTo(parent)
	child3.AttachTo(parent)
	child4.AttachTo(parent)

	child1.Consume(100)
	child2.Consume(2 * 1024)
	child3.Consume(3 * 1024 * 1024)
	child4.Consume(4 * 1024 * 1024 * 1024)

	require.Equal(t, parent.String(), `
"1"{
  "consumed": 4.00 GB
  "2"{
    "quota": 1000 Bytes
    "consumed": 100 Bytes
  }
  "3"{
    "consumed": 2 KB
  }
  "4"{
    "consumed": 3 MB
  }
  "5"{
    "consumed": 4 GB
  }
}
`)
}

func TestMaxConsumed(t *testing.T) {
	r := NewTracker(1, -1)
	c1 := NewTracker(2, -1)
	c2 := NewTracker(3, -1)
	cc1 := NewTracker(4, -1)

	c1.AttachTo(r)
	c2.AttachTo(r)
	cc1.AttachTo(c1)

	ts := []*Tracker{r, c1, c2, cc1}
	var consumed, maxConsumed int64
	for range 10 {
		tracker := ts[rand.Intn(len(ts))]
		b := rand.Int63n(1000) - 500
		if consumed+b < 0 {
			b = -consumed
		}
		consumed += b
		tracker.Consume(b)
		maxConsumed = max(maxConsumed, consumed)

		require.Equal(t, consumed, r.BytesConsumed())
		require.Equal(t, maxConsumed, r.MaxConsumed())
	}
}

func TestGlobalTracker(t *testing.T) {
	r := NewGlobalTracker(1, -1)
	c1 := NewTracker(2, -1)
	c2 := NewTracker(3, -1)
	c1.Consume(100)
	c2.Consume(200)

	c1.AttachToGlobalTracker(r)
	c2.AttachToGlobalTracker(r)
	require.Equal(t, int64(300), r.BytesConsumed())
	require.Equal(t, r, c1.getParent())
	require.Equal(t, r, c2.getParent())
	require.Equal(t, 0, len(r.mu.children))

	c1.DetachFromGlobalTracker()
	c2.DetachFromGlobalTracker()
	require.Equal(t, int64(0), r.BytesConsumed())
	require.Nil(t, c1.getParent())
	require.Nil(t, c2.getParent())
	require.Equal(t, 0, len(r.mu.children))

	defer func() {
		v := recover()
		require.Equal(t, "Attach to a non-GlobalTracker", v)
	}()
	commonTracker := NewTracker(4, -1)
	c1.AttachToGlobalTracker(commonTracker)

	c1.AttachTo(commonTracker)
	require.Equal(t, int64(100), commonTracker.BytesConsumed())
	require.Equal(t, 1, len(commonTracker.mu.children))
	require.Equal(t, commonTracker, c1.getParent())

	c1.AttachToGlobalTracker(r)
	require.Equal(t, int64(0), commonTracker.BytesConsumed())
	require.Equal(t, 0, len(commonTracker.mu.children))
	require.Equal(t, int64(100), r.BytesConsumed())
	require.Equal(t, r, c1.getParent())
	require.Equal(t, 0, len(r.mu.children))

	defer func() {
		v := recover()
		require.Equal(t, "Detach from a non-GlobalTracker", v)
	}()
	c2.AttachTo(commonTracker)
	c2.DetachFromGlobalTracker()
}

func parseByteUnit(str string) (int64, error) {
	u := strings.TrimSpace(str)
	switch u {
	case "GB":
		return byteSizeGB, nil
	case "MB":
		return byteSizeMB, nil
	case "KB":
		return byteSizeKB, nil
	case "Bytes":
		return byteSizeBB, nil
	}
	return 0, errors.New("invalid byte unit: " + str)
}

func parseByte(str string) (int64, error) {
	vBuf := make([]byte, 0, len(str))
	uBuf := make([]byte, 0, 2)
	b := int64(0)
	for _, v := range str {
		if (v >= '0' && v <= '9') || v == '.' {
			vBuf = append(vBuf, byte(v))
		} else if v != ' ' {
			uBuf = append(uBuf, byte(v))
		}
	}
	unit, err := parseByteUnit(string(uBuf))
	if err != nil {
		return 0, err
	}
	v, err := strconv.ParseFloat(string(vBuf), 64)
	if err != nil {
		return 0, err
	}
	b = int64(v * float64(unit))
	return b, nil
}

func TestFormatBytesWithPrune(t *testing.T) {
	cases := []struct {
		b string
		s string
	}{
		{"0 Bytes", "0 Bytes"},
		{"1 Bytes", "1 Bytes"},
		{"9 Bytes", "9 Bytes"},
		{"10 Bytes", "10 Bytes"},
		{"999 Bytes", "999 Bytes"},
		{"1 KB", "1024 Bytes"},
		{"1.123 KB", "1.12 KB"},
		{"1.023 KB", "1.02 KB"},
		{"1.003 KB", "1.00 KB"},
		{"10.456 KB", "10.5 KB"},
		{"10.956 KB", "11.0 KB"},
		{"999.056 KB", "999.1 KB"},
		{"999.988 KB", "1000.0 KB"},
		{"1.123 MB", "1.12 MB"},
		{"1.023 MB", "1.02 MB"},
		{"1.003 MB", "1.00 MB"},
		{"10.456 MB", "10.5 MB"},
		{"10.956 MB", "11.0 MB"},
		{"999.056 MB", "999.1 MB"},
		{"999.988 MB", "1000.0 MB"},
		{"1.123 GB", "1.12 GB"},
		{"1.023 GB", "1.02 GB"},
		{"1.003 GB", "1.00 GB"},
		{"10.456 GB", "10.5 GB"},
		{"10.956 GB", "11.0 GB"},
		{"9.412345 MB", "9.41 MB"},
		{"10.412345 MB", "10.4 MB"},
		{"5.999 GB", "6.00 GB"},
		{"100.46 KB", "100.5 KB"},
		{"18.399999618530273 MB", "18.4 MB"},
		{"9.15999984741211 MB", "9.16 MB"},
	}
	for _, ca := range cases {
		b, err := parseByte(ca.b)
		require.NoError(t, err)
		result := FormatBytes(b)
		require.Equalf(t, ca.s, result, "input: %v\n", ca.b)
	}
}

func TestErrorCode(t *testing.T) {
	require.Equal(t, errno.ErrMemExceedThreshold, int(terror.ToSQLError(errMemExceedThreshold).Code))
}

func TestOOMActionPriority(t *testing.T) {
	tracker := NewTracker(1, 100)
	// make sure no panic here.
	tracker.Consume(10000)

	tracker = NewTracker(1, 1)
	tracker.actionMuForHardLimit.actionOnExceed = nil
	n := 100
	actions := make([]*mockAction, n)
	for i := range n {
		actions[i] = &mockAction{priority: int64(i)}
	}

	randomShuffle := make([]int, n)
	for i := range n {
		randomShuffle[i] = i
		pos := rand.Int() % (i + 1)
		randomShuffle[i], randomShuffle[pos] = randomShuffle[pos], randomShuffle[i]
	}

	for i := range n {
		tracker.FallbackOldAndSetNewAction(actions[randomShuffle[i]])
	}
	for i := n - 1; i >= 0; i-- {
		tracker.Consume(100)
		for j := n - 1; j >= 0; j-- {
			if j >= i {
				require.True(t, actions[j].called)
			} else {
				require.False(t, actions[j].called)
			}
		}
	}
}
