/******************************************************************************
 * Copyright (c)  2022 PingCAP                                                *
 * 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.                                             *
 ******************************************************************************/

/*******************************************************************************
 * @File: manager_test.go
 * @Description:
 * @Author: jiangxunyu@pingcap.com
 * @Version: 1.0.0
 * @Date: 2022/6/16 17:01
*******************************************************************************/

package globalservice

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

	"github.com/stretchr/testify/assert"
	"github.com/tidbcloud/observability/pkg/common/constants"
	"github.com/tidbcloud/observability/pkg/common/metrics"
	"github.com/tidbcloud/observability/pkg/globalservice/structs"
	mock_meta_manager "github.com/tidbcloud/observability/test/mockmetamanager"
	"go.uber.org/mock/gomock"
	"k8s.io/apimachinery/pkg/watch"
)

var globalMetaCacheStr = `
{
	"vendors": {
		"aws": [
			{
				"region": "us-west-1",
				"routers": [{
					"dnsAddress": "http://127.0.0.1:8081",
					"primary": true
				}]
			},
			{
				"region": "us-west-2",
				"routers": [{
					"dnsAddress": "http://127.0.0.1:8082",
					"primary": true,
					"logicalRegionName": "us-west-2"
				},{
					"dnsAddress": "http://127.0.0.1:8083",
					"logicalRegionName": "us-west-2-byoc"
				}]
			}
		]
	},
	"auth0": {
        "issuerUrl": "https://test.us.auth0.com/",
        "audience": "https://test.us.auth0.com/api/v2/"
	},
	"routes": {
		"aws-us-west-2": {
				".*": "us-west-2",
				"byoc:.*:.*": "us-west-2-byoc"
			}
	}
}
`

var globalMetaCache = structs.GlobalServiceMeta{
	Vendors: map[constants.ProvidedVendor][]structs.RegionInfo{
		"aws": {
			{
				Region: "us-west-1",
				Routers: []structs.RegionalRouter{
					{
						DnsAddress: "http://127.0.0.1:8081",
						Primary:    true,
					},
				},
			},
			{
				Region: "us-west-2",
				Routers: []structs.RegionalRouter{
					{
						DnsAddress: "http://127.0.0.1:8082",
						Primary:    true,
					},
					{
						DnsAddress: "http://127.0.0.1:8083",
					},
				},
			},
		},
	},
}

// mockWatcher implements watch.Interface for testing
type mockWatcher struct {
	resultChan chan watch.Event
	stopped    bool
}

func newMockWatcher() *mockWatcher {
	return &mockWatcher{
		resultChan: make(chan watch.Event),
	}
}

func (m *mockWatcher) Stop() {
	if !m.stopped {
		m.stopped = true
		close(m.resultChan)
	}
}

func (m *mockWatcher) ResultChan() <-chan watch.Event {
	return m.resultChan
}

func Test_GetGlobalManager(t *testing.T) {
	t.Run("success", func(t *testing.T) {
		manager := GetGlobalManager()
		assert.NotNil(t, manager)
	})
}

func Test_GetGlobalMetaCache(t *testing.T) {
	t.Run("success", func(t *testing.T) {
		meta := GetGlobalManager().GetGlobalMetaCache()
		assert.NotNil(t, meta)
	})
}

func Test_EditGlobalMetaCache(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	mockMetaService := mock_meta_manager.NewMockMetaService(ctrl)
	globalManager := &GlobalManager{
		globalMetaService: mockMetaService,
	}
	mockMetaService.EXPECT().WatchMeta(gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes()

	t.Run("success", func(t *testing.T) {
		mockMetaService.EXPECT().UpdateMeta(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil)
		mockMetaService.EXPECT().DetailMeta(gomock.Any(), gomock.Any()).Return(globalMetaCacheStr, nil)

		err := globalManager.EditGlobalMetaCache(context.TODO(), globalMetaCache)
		assert.NoError(t, err)
	})

	t.Run("edit meta error", func(t *testing.T) {
		mockMetaService.EXPECT().UpdateMeta(gomock.Any(), gomock.Any(), gomock.Any()).
			Return(errors.New("edit meta failed"))

		err := globalManager.EditGlobalMetaCache(context.TODO(), globalMetaCache)
		assert.Error(t, err)
	})

	t.Run("load meta error", func(t *testing.T) {
		mockMetaService.EXPECT().UpdateMeta(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil)
		mockMetaService.EXPECT().DetailMeta(gomock.Any(), gomock.Any()).
			Return("", errors.New("load meta failed"))

		err := globalManager.EditGlobalMetaCache(context.TODO(), globalMetaCache)
		assert.Error(t, err)
	})
}

func Test_CreateGlobalMetaCache(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()
	metrics.SetupMetrics()
	mockMetaService := mock_meta_manager.NewMockMetaService(ctrl)
	globalManager := &GlobalManager{
		globalMetaService: mockMetaService,
	}
	t.Run("success", func(t *testing.T) {
		mockMetaService.EXPECT().CreateMeta(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil)
		mockMetaService.EXPECT().DetailMeta(gomock.Any(), gomock.Any()).Return(globalMetaCacheStr, nil)

		err := globalManager.CreateGlobalMetaCache(context.TODO(), globalMetaCache)
		assert.NoError(t, err)
	})

	t.Run("create meta error", func(t *testing.T) {
		mockMetaService.EXPECT().CreateMeta(gomock.Any(), gomock.Any(), gomock.Any()).
			Return(errors.New("create meta failed"))

		err := globalManager.CreateGlobalMetaCache(context.TODO(), globalMetaCache)
		assert.Error(t, err)
	})

	t.Run("load meta error", func(t *testing.T) {
		mockMetaService.EXPECT().CreateMeta(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil)
		mockMetaService.EXPECT().DetailMeta(gomock.Any(), gomock.Any()).
			Return("", errors.New("load meta failed"))

		err := globalManager.CreateGlobalMetaCache(context.TODO(), globalMetaCache)
		assert.Error(t, err)
	})
}

func Test_loadGlobalMetaCache(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	mockMetaService := mock_meta_manager.NewMockMetaService(ctrl)

	globalManager := &GlobalManager{
		globalMetaService: mockMetaService,
	}
	globalManager.logicalRegionRouter = NewLogicalRegionRouter(globalManager)

	t.Run("success", func(t *testing.T) {
		mockMetaService.EXPECT().DetailMeta(gomock.Any(), gomock.Any()).
			Return(globalMetaCacheStr, nil)

		err := globalManager.loadGlobalMetaCache(context.TODO())
		assert.NoError(t, err)
	})

	t.Run("json unmarshal error", func(t *testing.T) {
		mockMetaService.EXPECT().DetailMeta(gomock.Any(), gomock.Any()).
			Return(`json unmarshal error`, nil)

		err := globalManager.loadGlobalMetaCache(context.TODO())
		assert.Error(t, err)
	})

	t.Run("detail meta error", func(t *testing.T) {
		mockMetaService.EXPECT().DetailMeta(gomock.Any(), gomock.Any()).
			Return("", errors.New("detail meta failed"))

		err := globalManager.loadGlobalMetaCache(context.TODO())
		assert.Error(t, err)
	})
}

func Test_GetRegionalRouteInfo(t *testing.T) {
	t.Run("logical region routing enabled - success", func(t *testing.T) {
		ctrl := gomock.NewController(t)
		defer ctrl.Finish()

		mockLogicalRouter := mock_meta_manager.NewMockLogicalRegionRouter(ctrl)
		expectedRouter := &structs.RegionalRouter{
			DnsAddress:        "http://logical-region.example.com",
			LogicalRegionName: "logical-us-west-2",
			Primary:           true,
		}

		mockLogicalRouter.EXPECT().
			IsLogicalRegionEnabled().
			Return(true)
		mockLogicalRouter.EXPECT().
			GetLogicalRegionRouteInfo(gomock.Any(), gomock.Any()).
			Return(expectedRouter, nil)

		globalManager := &GlobalManager{
			globalMetaCache:     &globalMetaCache,
			logicalRegionRouter: mockLogicalRouter,
		}

		routingCtx := structs.RoutingContext{
			Vendor:   constants.AWS,
			Region:   "us-west-2",
			BizType:  "tidbcloud",
			TenantID: "12345",
		}

		routeInfo, err := globalManager.GetRegionalRouteInfo(context.TODO(), routingCtx)
		assert.NoError(t, err)
		assert.Equal(t, expectedRouter, routeInfo)
	})

	t.Run("logical region routing enabled - fallback to physical routing", func(t *testing.T) {
		ctrl := gomock.NewController(t)
		defer ctrl.Finish()

		mockLogicalRouter := mock_meta_manager.NewMockLogicalRegionRouter(ctrl)

		mockLogicalRouter.EXPECT().
			IsLogicalRegionEnabled().
			Return(true)
		mockLogicalRouter.EXPECT().
			GetLogicalRegionRouteInfo(gomock.Any(), gomock.Any()).
			Return(nil, errors.New("logical routing failed"))

		globalManager := &GlobalManager{
			globalMetaCache:     &globalMetaCache,
			logicalRegionRouter: mockLogicalRouter,
		}

		routingCtx := structs.RoutingContext{
			Vendor:   constants.AWS,
			Region:   "us-west-1",
			BizType:  "tidbcloud",
			TenantID: "12345",
		}

		routeInfo, err := globalManager.GetRegionalRouteInfo(context.TODO(), routingCtx)
		assert.NoError(t, err)
		assert.NotNil(t, routeInfo)
		assert.Equal(t, "http://127.0.0.1:8081", routeInfo.DnsAddress)
	})

	t.Run("logical region routing disabled", func(t *testing.T) {
		ctrl := gomock.NewController(t)
		defer ctrl.Finish()

		mockLogicalRouter := mock_meta_manager.NewMockLogicalRegionRouter(ctrl)

		mockLogicalRouter.EXPECT().
			IsLogicalRegionEnabled().
			Return(false)

		globalManager := &GlobalManager{
			globalMetaCache:     &globalMetaCache,
			logicalRegionRouter: mockLogicalRouter,
		}

		routingCtx := structs.RoutingContext{
			Vendor:   constants.AWS,
			Region:   "us-west-1",
			BizType:  "tidbcloud",
			TenantID: "12345",
		}

		routeInfo, err := globalManager.GetRegionalRouteInfo(context.TODO(), routingCtx)
		assert.NoError(t, err)
		assert.NotNil(t, routeInfo)
		assert.Equal(t, "http://127.0.0.1:8081", routeInfo.DnsAddress)
	})

	t.Run("missing biz type and tenant id - physical routing", func(t *testing.T) {
		globalManager := &GlobalManager{
			globalMetaCache: &globalMetaCache,
		}

		routingCtx := structs.RoutingContext{
			Vendor: constants.AWS,
			Region: "us-west-1",
			// Missing BizType and TenantID
		}

		routeInfo, err := globalManager.GetRegionalRouteInfo(context.TODO(), routingCtx)
		assert.NoError(t, err)
		assert.NotNil(t, routeInfo)
		assert.Equal(t, "http://127.0.0.1:8081", routeInfo.DnsAddress)
	})

	t.Run("explicit logical region override wins before logical router fallback", func(t *testing.T) {
		globalManager := &GlobalManager{
			globalMetaCache: &structs.GlobalServiceMeta{
				Vendors: map[constants.ProvidedVendor][]structs.RegionInfo{
					constants.AWS: {
						{
							Region: "us-west-2",
							Routers: []structs.RegionalRouter{
								{
									DnsAddress:        "http://127.0.0.1:8082",
									LogicalRegionName: "us-west-2",
									Primary:           true,
								},
								{
									DnsAddress:        "http://127.0.0.1:8083",
									LogicalRegionName: "us-west-2-secondary",
								},
							},
						},
					},
				},
			},
		}

		routingCtx := structs.RoutingContext{
			Vendor:        constants.AWS,
			Region:        "us-west-2",
			LogicalRegion: "us-west-2-secondary",
		}

		routeInfo, err := globalManager.GetRegionalRouteInfo(context.TODO(), routingCtx)
		assert.NoError(t, err)
		assert.Equal(t, "us-west-2-secondary", routeInfo.LogicalRegionName)
	})
}

func Test_getRegionalRouteInfo(t *testing.T) {
	t.Run("success - single router", func(t *testing.T) {
		globalManager := &GlobalManager{
			globalMetaCache: &globalMetaCache,
		}

		routingCtx := structs.RoutingContext{
			Vendor: constants.AWS,
			Region: "us-west-1",
		}

		routeInfo, err := globalManager.getRegionalRouteInfo(context.TODO(), routingCtx)
		assert.NoError(t, err)
		assert.NotNil(t, routeInfo)
		assert.Equal(t, "http://127.0.0.1:8081", routeInfo.DnsAddress)
		assert.True(t, routeInfo.Primary)
	})

	t.Run("success - multiple routers with primary", func(t *testing.T) {
		globalManager := &GlobalManager{
			globalMetaCache: &globalMetaCache,
		}

		routingCtx := structs.RoutingContext{
			Vendor: constants.AWS,
			Region: "us-west-2",
		}

		routeInfo, err := globalManager.getRegionalRouteInfo(context.TODO(), routingCtx)
		assert.NoError(t, err)
		assert.NotNil(t, routeInfo)
		assert.Equal(t, "http://127.0.0.1:8082", routeInfo.DnsAddress)
		assert.True(t, routeInfo.Primary)
	})

	t.Run("vendor not found", func(t *testing.T) {
		globalManager := &GlobalManager{
			globalMetaCache: &globalMetaCache,
		}

		routingCtx := structs.RoutingContext{
			Vendor: "nonexistent",
			Region: "us-west-1",
		}

		routeInfo, err := globalManager.getRegionalRouteInfo(context.TODO(), routingCtx)
		assert.Error(t, err)
		assert.Nil(t, routeInfo)
		assert.Contains(t, err.Error(), "failed to get the routing address")
	})

	t.Run("region not found", func(t *testing.T) {
		globalManager := &GlobalManager{
			globalMetaCache: &globalMetaCache,
		}

		routingCtx := structs.RoutingContext{
			Vendor: constants.AWS,
			Region: "nonexistent-region",
		}

		routeInfo, err := globalManager.getRegionalRouteInfo(context.TODO(), routingCtx)
		assert.Error(t, err)
		assert.Nil(t, routeInfo)
		assert.Contains(t, err.Error(), "failed to get the routing address")
	})

	t.Run("empty global meta cache", func(t *testing.T) {
		emptyCache := structs.GlobalServiceMeta{
			Vendors: map[constants.ProvidedVendor][]structs.RegionInfo{},
		}
		globalManager := &GlobalManager{
			globalMetaCache: &emptyCache,
		}

		routingCtx := structs.RoutingContext{
			Vendor: constants.AWS,
			Region: "us-west-1",
		}

		routeInfo, err := globalManager.getRegionalRouteInfo(context.TODO(), routingCtx)
		assert.Error(t, err)
		assert.Nil(t, routeInfo)
		assert.Contains(t, err.Error(), "failed to get the routing address")
	})
}

func Test_WatchGlobalMetaCache(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	t.Run("watch meta error", func(t *testing.T) {
		mockMetaService := mock_meta_manager.NewMockMetaService(ctrl)
		globalManager := &GlobalManager{
			globalMetaService: mockMetaService,
			globalMetaCache:   &globalMetaCache,
		}

		// Use a context with timeout to prevent infinite retry loop
		ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
		defer cancel()

		// Expect multiple calls due to retry logic
		mockMetaService.EXPECT().WatchMeta(gomock.Any(), gomock.Any()).
			Return(nil, errors.New("watch meta failed")).AnyTimes()

		err := globalManager.WatchGlobalMetaCache(ctx)
		assert.Error(t, err)
		assert.Equal(t, context.DeadlineExceeded, err)
	})

	t.Run("context cancelled", func(t *testing.T) {
		mockMetaService := mock_meta_manager.NewMockMetaService(ctrl)
		globalManager := &GlobalManager{
			globalMetaService: mockMetaService,
			globalMetaCache:   &globalMetaCache,
		}

		// Create a context that's already cancelled
		ctx, cancel := context.WithCancel(context.Background())
		cancel()

		err := globalManager.WatchGlobalMetaCache(ctx)
		assert.Error(t, err)
		assert.Equal(t, context.Canceled, err)
	})

	t.Run("watcher channel closed", func(t *testing.T) {
		mockMetaService := mock_meta_manager.NewMockMetaService(ctrl)
		globalManager := &GlobalManager{
			globalMetaService: mockMetaService,
			globalMetaCache:   &globalMetaCache,
		}

		// Create a mock watcher that immediately closes its channel
		mockWatcher := newMockWatcher()
		mockWatcher.Stop() // Close the channel immediately

		// Use a context with timeout to prevent infinite loop
		ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
		defer cancel()

		mockMetaService.EXPECT().WatchMeta(gomock.Any(), gomock.Any()).
			Return(mockWatcher, nil).AnyTimes()

		err := globalManager.WatchGlobalMetaCache(ctx)
		assert.Error(t, err)
		assert.Equal(t, context.DeadlineExceeded, err)
	})
}
