// Copyright 2024 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 util

import (
	"context"
	"os"
	"path/filepath"
	"testing"
	"time"

	infoschemacontext "github.com/pingcap/tidb/pkg/infoschema/context"
	"github.com/pingcap/tidb/pkg/meta/model"
	pmodel "github.com/pingcap/tidb/pkg/parser/model"
	"github.com/stretchr/testify/require"
	clientv3 "go.etcd.io/etcd/client/v3"
)

func TestFolderNotEmpty(t *testing.T) {
	tmp := t.TempDir()
	require.False(t, FolderNotEmpty(tmp))
	require.False(t, FolderNotEmpty(filepath.Join(tmp, "not-exist")))

	f, err := os.Create(filepath.Join(tmp, "test-file"))
	require.NoError(t, err)
	require.NoError(t, f.Close())
	require.True(t, FolderNotEmpty(tmp))
}

func TestEtcdKeyOpRetryBackoff(t *testing.T) {
	tests := []struct {
		retryCnt int
		base     time.Duration
		min      time.Duration
		max      time.Duration
	}{
		{retryCnt: 0, base: 30 * time.Millisecond, min: 30 * time.Millisecond, max: 30 * time.Millisecond},
		{retryCnt: 1, base: 60 * time.Millisecond, min: 30 * time.Millisecond, max: 60 * time.Millisecond},
		{retryCnt: 2, base: 120 * time.Millisecond, min: 60 * time.Millisecond, max: 120 * time.Millisecond},
		{retryCnt: 3, base: 240 * time.Millisecond, min: 120 * time.Millisecond, max: 240 * time.Millisecond},
		{retryCnt: 4, base: 480 * time.Millisecond, min: 240 * time.Millisecond, max: 480 * time.Millisecond},
		{retryCnt: 5, base: 960 * time.Millisecond, min: 480 * time.Millisecond, max: 960 * time.Millisecond},
		{retryCnt: 6, base: 1920 * time.Millisecond, min: 960 * time.Millisecond, max: 1920 * time.Millisecond},
		{retryCnt: 7, base: 3840 * time.Millisecond, min: 1920 * time.Millisecond, max: 3840 * time.Millisecond},
		{retryCnt: 8, base: 7680 * time.Millisecond, min: 3840 * time.Millisecond, max: 7680 * time.Millisecond},
		{retryCnt: 9, base: 10 * time.Second, min: 5 * time.Second, max: 10 * time.Second},
		{retryCnt: 10, base: 10 * time.Second, min: 5 * time.Second, max: 10 * time.Second},
	}

	for _, tt := range tests {
		require.Equal(t, tt.base, etcdKeyOpRetryBackoffBase(tt.retryCnt))
		backoff := etcdKeyOpRetryBackoff(tt.retryCnt)
		require.GreaterOrEqual(t, backoff, tt.min)
		require.LessOrEqual(t, backoff, tt.max)
	}
}

func TestSleepEtcdKeyOpRetryBackoffCanceled(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	cancel()

	start := time.Now()
	err := sleepEtcdKeyOpRetryBackoff(ctx, 10)
	require.ErrorIs(t, err, context.Canceled)
	require.Less(t, time.Since(start), 100*time.Millisecond)
}

type mockMetaOnlyInfoSchema struct {
	infoschemacontext.MetaOnlyInfoSchema
	tables []infoschemacontext.TableInfoResult
}

func (m mockMetaOnlyInfoSchema) ListTablesWithSpecialAttribute(
	filter infoschemacontext.SpecialAttributeFilter,
) []infoschemacontext.TableInfoResult {
	ret := make([]infoschemacontext.TableInfoResult, 0, len(m.tables))
	for _, db := range m.tables {
		tableInfos := make([]*model.TableInfo, 0, len(db.TableInfos))
		for _, tbl := range db.TableInfos {
			if filter(tbl) {
				tableInfos = append(tableInfos, tbl)
			}
		}
		if len(tableInfos) > 0 {
			ret = append(ret, infoschemacontext.TableInfoResult{
				DBName:     db.DBName,
				TableInfos: tableInfos,
			})
		}
	}
	return ret
}

func TestGetDeadLockedTablesNoLockSkipsEtcd(t *testing.T) {
	checker := NewDeadTableLockChecker(&clientv3.Client{})
	is := mockMetaOnlyInfoSchema{
		tables: []infoschemacontext.TableInfoResult{
			{
				DBName: pmodel.NewCIStr("test"),
				TableInfos: []*model.TableInfo{
					{ID: 1, Name: pmodel.NewCIStr("t")},
				},
			},
		},
	}

	deadLockTables, err := checker.GetDeadLockedTables(context.Background(), is)
	require.NoError(t, err)
	require.Empty(t, deadLockTables)
}
