/*
 * Licensed to the 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.
 * The 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 base

import (
	"context"
	"database/sql"
	"sync"
	"testing"
	"time"

	"github.com/DATA-DOG/go-sqlmock"
	"github.com/agiledragon/gomonkey/v2"
	"github.com/go-sql-driver/mysql"
	"github.com/stretchr/testify/assert"
	"seata.apache.org/seata-go/pkg/datasource/sql/types"
	"seata.apache.org/seata-go/testdata"
)

var (
	capacity      int32 = 1024
	EexpireTime         = 15 * time.Minute
	tableMetaOnce sync.Once
)

type mockTrigger struct {
}

// LoadOne simulates loading table metadata, including id, name, and age columns.
func (m *mockTrigger) LoadOne(ctx context.Context, dbName string, table string, conn *sql.Conn) (*types.TableMeta, error) {

	return &types.TableMeta{
		TableName: table,
		Columns: map[string]types.ColumnMeta{
			"id":   {ColumnName: "id"},
			"name": {ColumnName: "name"},
			"age":  {ColumnName: "age"},
		},
		Indexs: map[string]types.IndexMeta{
			"id": {
				Name:    "PRIMARY",
				IType:   types.IndexTypePrimaryKey,
				Columns: []types.ColumnMeta{{ColumnName: "id"}},
			},
			"id_name_age": {
				Name:    "name_age_idx",
				IType:   types.IndexUnique,
				Columns: []types.ColumnMeta{{ColumnName: "name"}, {ColumnName: "age"}},
			},
		},
		ColumnNames: []string{"id", "name", "age"},
	}, nil
}

func (m *mockTrigger) LoadAll(ctx context.Context, dbName string, conn *sql.Conn, tables ...string) ([]types.TableMeta, error) {
	return nil, nil
}

func TestBaseTableMetaCache_refresh(t *testing.T) {
	type fields struct {
		expireDuration time.Duration
		capity         int32
		size           int32
		cache          map[string]*entry
		cancel         context.CancelFunc
		trigger        trigger
		db             *sql.DB
		cfg            *mysql.Config
	}
	type args struct {
		ctx context.Context
	}
	ctx, cancel := context.WithCancel(context.Background())
	tests := []struct {
		name   string
		fields fields
		args   args
		want   types.TableMeta
	}{
		{
			name: "test1",
			fields: fields{
				capity:         capacity,
				size:           0,
				expireDuration: EexpireTime,
				cache: map[string]*entry{
					"test": {
						value:      types.TableMeta{},
						lastAccess: time.Now(),
					},
				},
				cancel:  cancel,
				trigger: &mockTrigger{},
				cfg:     &mysql.Config{},
				db:      &sql.DB{},
			},
			args: args{ctx: ctx},
			want: testdata.MockWantTypesMeta("test"),
		},
		{
			name: "test2",
			fields: fields{
				capity:         capacity,
				size:           0,
				expireDuration: EexpireTime,
				cache: map[string]*entry{
					"TEST": {
						value:      types.TableMeta{},
						lastAccess: time.Now(),
					},
				},
				cancel:  cancel,
				trigger: &mockTrigger{},
				cfg:     &mysql.Config{},
				db:      &sql.DB{},
			},
			args: args{ctx: ctx},
			want: testdata.MockWantTypesMeta("TEST"),
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {

			connStub := gomonkey.ApplyMethodFunc(tt.fields.db, "Conn",
				func(_ context.Context) (*sql.Conn, error) {
					return &sql.Conn{}, nil
				})

			defer connStub.Reset()

			loadAllStub := gomonkey.ApplyMethodFunc(tt.fields.trigger, "LoadAll",
				func(_ context.Context, _ string, _ *sql.Conn, _ ...string) ([]types.TableMeta, error) {
					return []types.TableMeta{tt.want}, nil
				})

			defer loadAllStub.Reset()

			c := &BaseTableMetaCache{
				expireDuration: tt.fields.expireDuration,
				capity:         tt.fields.capity,
				size:           tt.fields.size,
				cache:          tt.fields.cache,
				cancel:         tt.fields.cancel,
				trigger:        tt.fields.trigger,
				db:             tt.fields.db,
				cfg:            tt.fields.cfg,
			}
			go c.refresh(tt.args.ctx)
			time.Sleep(time.Second * 3)
			c.lock.RLock()
			defer c.lock.RUnlock()
			assert.Equal(t, c.cache[func() string {
				if tt.name == "test2" {
					return "TEST"
				}
				return "test"
			}()].value, tt.want)
		})
	}
}

func TestBaseTableMetaCache_GetTableMeta(t *testing.T) {
	var (
		tableMeta1  types.TableMeta
		tableMeta2  types.TableMeta
		columns     = make(map[string]types.ColumnMeta)
		index       = make(map[string]types.IndexMeta)
		index2      = make(map[string]types.IndexMeta)
		columnMeta1 []types.ColumnMeta
		columnMeta2 []types.ColumnMeta
		ColumnNames []string
	)
	columnId := types.ColumnMeta{
		ColumnDef:  nil,
		ColumnName: "id",
	}
	columnName := types.ColumnMeta{
		ColumnDef:  nil,
		ColumnName: "name",
	}
	columnAge := types.ColumnMeta{
		ColumnDef:  nil,
		ColumnName: "age",
	}
	columns["id"] = columnId
	columns["name"] = columnName
	columns["age"] = columnAge
	columnMeta1 = append(columnMeta1, columnId)
	columnMeta2 = append(columnMeta2, columnName, columnAge)
	index["id"] = types.IndexMeta{
		Name:    "PRIMARY",
		IType:   types.IndexTypePrimaryKey,
		Columns: columnMeta1,
	}
	index["id_name_age"] = types.IndexMeta{
		Name:    "name_age_idx",
		IType:   types.IndexUnique,
		Columns: columnMeta2,
	}

	ColumnNames = []string{"id", "name", "age"}
	tableMeta1 = types.TableMeta{
		TableName:   "t_user1",
		Columns:     columns,
		Indexs:      index,
		ColumnNames: ColumnNames,
	}

	index2["id_name_age"] = types.IndexMeta{
		Name:    "name_age_idx",
		IType:   types.IndexUnique,
		Columns: columnMeta2,
	}

	tableMeta2 = types.TableMeta{
		TableName:   "T_USER2",
		Columns:     columns,
		Indexs:      index2,
		ColumnNames: ColumnNames,
	}
	tests := []types.TableMeta{tableMeta1, tableMeta2}
	// Use sqlmock to simulate a database connection
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("Failed to create sqlmock: %v", err)
	}
	defer db.Close()
	for _, tt := range tests {
		t.Run(tt.TableName, func(t *testing.T) {
			mockTrigger := &mockTrigger{}
			// Mock a query response
			mock.ExpectQuery("SELECT").WillReturnRows(sqlmock.NewRows([]string{"id", "name", "age"}))
			// Create a mock database connection
			conn, err := db.Conn(context.Background())
			if err != nil {
				t.Fatalf("Failed to get connection: %v", err)
			}
			defer conn.Close()
			cache := &BaseTableMetaCache{
				trigger: mockTrigger,
				cache: map[string]*entry{
					"t_user1": {
						value:      tableMeta1,
						lastAccess: time.Now(),
					},
					"T_USER2": {
						value:      tableMeta2,
						lastAccess: time.Now(),
					},
				},
				lock: sync.RWMutex{},
			}

			meta, _ := cache.GetTableMeta(context.Background(), "db", tt.TableName, conn)

			if meta.TableName != tt.TableName {
				t.Errorf("GetTableMeta() got TableName = %v, want %v", meta.TableName, tt.TableName)
			}
			// Ensure the retrieved table is cached
			cache.lock.RLock()
			_, cached := cache.cache[tt.TableName]
			cache.lock.RUnlock()

			if !cached {
				t.Errorf("GetTableMeta() got TableName = %v, want %v", meta.TableName, tt.TableName)
			}
		})
	}
}
