package proxy

import (
	"context"
	"fmt"
	"net/http"
	"net/url"
	"sync"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"

	"github.com/pingcap-inc/clinic/pkg/db"
	"github.com/pingcap-inc/clinic/pkg/o11y"
	o11yurls "github.com/pingcap-inc/clinic/pkg/o11y/urls"
)

func TestIsSharedResourceDashboardRequest(t *testing.T) {
	tests := []struct {
		name         string
		referer      string
		target       string
		dashboardUID string
		want         bool
	}{
		{
			name:         "Grafana dashboard UID header",
			target:       "https://clinic.example.com/metric/query/api/v1/query_range",
			dashboardUID: SharedResourceDashboardUID,
			want:         true,
		},
		{
			name:    "shared resource dashboard UID",
			referer: "https://clinic.example.com/grafana/d/5592da408d0ffe87nextgenv9_0_x/sharedscope?orgId=1",
			target:  "https://clinic.example.com/metric/query/api/v1/query",
			want:    true,
		},
		{
			name:    "explicit dashboard scope",
			referer: "https://clinic.example.com/grafana/d/a-new-uid/sharedscope?var-shared_resource_scope=resource_pool",
			target:  "https://clinic.example.com/metric/query/api/v1/labels",
			want:    true,
		},
		{
			name:   "explicit request scope",
			target: "https://clinic.example.com/metric/query/api/v1/series?var-shared_resource_scope=resource_pool",
			want:   true,
		},
		{
			name:    "other dashboard",
			referer: "https://clinic.example.com/grafana/d/other/userscope",
			target:  "https://clinic.example.com/metric/query/api/v1/query_range",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			req, err := http.NewRequest(http.MethodGet, tt.target, nil)
			require.NoError(t, err)
			if tt.referer != "" {
				req.Header.Set("Referer", tt.referer)
			}
			if tt.dashboardUID != "" {
				req.Header.Set(SharedResourceDashboardUIDHeader, tt.dashboardUID)
			}
			require.Equal(t, tt.want, isSharedResourceDashboardRequest(req))
		})
	}
}

func TestRequestQueriesSharedPool(t *testing.T) {
	tests := []struct {
		name   string
		target string
		poolID string
		want   bool
	}{
		{
			name:   "query range for current resource pool",
			target: `https://clinic.example.com/metric/query/api/v1/query_range?query=sum(rate(tidb_server_query_total%7Bsharedpool_id%3D%22633337068600189325%22%2Cresult%3D%22OK%22%7D%5B5m%5D))`,
			poolID: "633337068600189325",
			want:   true,
		},
		{
			name:   "series matcher for current resource pool",
			target: `https://clinic.example.com/metric/query/api/v1/series?match%5B%5D=tidb_server_query_total%7Bsharedpool_id%3D%22633337068600189325%22%7D`,
			poolID: "633337068600189325",
			want:   true,
		},
		{
			name:   "different resource pool",
			target: `https://clinic.example.com/metric/query/api/v1/query_range?query=up%7Bsharedpool_id%3D%22other-pool%22%7D`,
			poolID: "633337068600189325",
		},
		{
			name:   "pool ID outside label matcher",
			target: `https://clinic.example.com/metric/query/api/v1/query_range?query=label_replace(up%2C%22x%22%2C%22633337068600189325%22%2C%22y%22%2C%22.%2A%22)`,
			poolID: "633337068600189325",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			req, err := http.NewRequest(http.MethodGet, tt.target, nil)
			require.NoError(t, err)
			require.Equal(t, tt.want, requestQueriesSharedPool(req, tt.poolID))
		})
	}
}

func TestVMAccountIDFromURL(t *testing.T) {
	accountID, err := vmAccountIDFromURL("https://metrics.example.com/api/v1/?extra_filters%5B%5D=%7Bvm_account_id%3D%22421%22%7D")
	require.NoError(t, err)
	require.Equal(t, "421", accountID)

	accountID, err = vmAccountIDFromURL("https://metrics.example.com/api/v1/?extra_filters%5B%5D=vm_account_id%3D%22435%22")
	require.NoError(t, err)
	require.Equal(t, "435", accountID)

	_, err = vmAccountIDFromURL("https://metrics.example.com/api/v1/")
	require.Error(t, err)
}

func TestBuildResourcePoolMetricsURL(t *testing.T) {
	raw := "https://metrics.example.com/internal/v2/metrics/multitenant/api/v1/?" +
		"extra_filters%5B%5D=%7Bvm_account_id%3D%22400%22%7D&" +
		"extra_filters%5B%5D=%7Bexisting%3D%22safe%22%7D&tenant=protected"
	got, err := buildResourcePoolMetricsURL(raw, "2015406318478692352", map[string]struct{}{
		"435": {},
		"400": {},
		"421": {},
	})
	require.NoError(t, err)
	u, err := url.Parse(got)
	require.NoError(t, err)
	require.Equal(t, "protected", u.Query().Get("tenant"))
	require.ElementsMatch(t, []string{
		`{existing="safe"}`,
		`{vm_account_id=~"^(400|421|435)$"}`,
		`{sharedpool_id="2015406318478692352"}`,
	}, u.Query()["extra_filters[]"])
}

type fakeMetricsTargetResolver struct {
	mu       sync.Mutex
	accounts map[string]string
	calls    map[string]int
}

func (f *fakeMetricsTargetResolver) ResolveMetricsTarget(
	_ context.Context,
	cluster *db.Cluster,
	_ o11yurls.MetricsScope,
	_ string,
) (*o11yurls.MetricsTarget, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.calls[cluster.ClusterID]++
	accountID, ok := f.accounts[cluster.ClusterID]
	if !ok {
		return nil, fmt.Errorf("account not found")
	}
	return &o11yurls.MetricsTarget{O11yCluster: &o11y.ClusterInfo{
		MultitenantQueryUri: "https://metrics.example.com/api/v1/?extra_filters%5B%5D=%7Bvm_account_id%3D%22" + accountID + "%22%7D",
	}}, nil
}

func TestResourcePoolMetricsTargetResolverMaintainsLazyRoutes(t *testing.T) {
	gormDB, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
	require.NoError(t, err)
	require.NoError(t, gormDB.AutoMigrate(&db.Cluster{}, &db.ResourcePoolO11yRoute{}))

	pool := &db.Cluster{
		ClusterID:    "pool-1",
		DeployTypeV2: o11y.DeployTypeV2EssentialV2ResourcePool,
	}
	logicalClusters := []db.Cluster{
		{ClusterID: "logic-1", ParentID: pool.ClusterID, DeployTypeV2: o11y.DeployTypeV2EssentialV2},
		{ClusterID: "logic-2", ParentID: pool.ClusterID, DeployTypeV2: o11y.DeployTypeV2EssentialV2},
	}
	require.NoError(t, gormDB.Create(&logicalClusters).Error)

	fake := &fakeMetricsTargetResolver{
		accounts: map[string]string{"logic-1": "421", "logic-2": "435"},
		calls:    make(map[string]int),
	}
	resolver := &resourcePoolMetricsTargetResolver{
		db:             gormDB,
		targetResolver: fake,
		routeTTL:       24 * time.Hour,
		now:            time.Now,
	}
	baseURL := "https://metrics.example.com/api/v1/?extra_filters%5B%5D=%7Bvm_account_id%3D%22400%22%7D"

	got, err := resolver.buildMetricsURL(context.Background(), pool, baseURL)
	require.NoError(t, err)
	gotURL, err := url.Parse(got)
	require.NoError(t, err)
	require.Contains(t, gotURL.Query()["extra_filters[]"], `{vm_account_id=~"^(400|421|435)$"}`)
	require.Equal(t, 1, fake.calls["logic-1"])
	require.Equal(t, 1, fake.calls["logic-2"])
	var routeCount int64
	require.NoError(t, gormDB.Model(&db.ResourcePoolO11yRoute{}).Count(&routeCount).Error)
	require.EqualValues(t, 2, routeCount)

	// Fresh persisted mappings avoid another O11y lookup.
	_, err = resolver.buildMetricsURL(context.Background(), pool, baseURL)
	require.NoError(t, err)
	require.Equal(t, 1, fake.calls["logic-1"])
	require.Equal(t, 1, fake.calls["logic-2"])

	// Historical resource-pool dashboards retain soft-deleted members.
	require.NoError(t, gormDB.Where("cluster_id = ?", logicalClusters[1].ClusterID).Delete(&db.Cluster{}).Error)
	got, err = resolver.buildMetricsURL(context.Background(), pool, baseURL)
	require.NoError(t, err)
	gotURL, err = url.Parse(got)
	require.NoError(t, err)
	require.Contains(t, gotURL.Query()["extra_filters[]"], `{vm_account_id=~"^(400|421|435)$"}`)
}

func TestResourcePoolMetricsTargetResolverIncludesSoftDeletedLogicalClusters(t *testing.T) {
	gormDB, err := gorm.Open(sqlite.Open("file:shared_deleted?mode=memory&cache=shared"), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
	require.NoError(t, err)
	require.NoError(t, gormDB.AutoMigrate(&db.Cluster{}, &db.ResourcePoolO11yRoute{}))

	pool := &db.Cluster{
		ClusterID:    "deleted-pool",
		DeployTypeV2: o11y.DeployTypeV2EssentialV2ResourcePool,
	}
	logicalCluster := &db.Cluster{
		ClusterID:    "deleted-logic",
		ParentID:     pool.ClusterID,
		DeployTypeV2: o11y.DeployTypeV2EssentialV2,
	}
	require.NoError(t, gormDB.Create(logicalCluster).Error)
	require.NoError(t, gormDB.Delete(logicalCluster).Error)

	fake := &fakeMetricsTargetResolver{
		accounts: map[string]string{logicalCluster.ClusterID: "421"},
		calls:    make(map[string]int),
	}
	resolver := &resourcePoolMetricsTargetResolver{
		db:             gormDB,
		targetResolver: fake,
		routeTTL:       24 * time.Hour,
		now:            time.Now,
	}
	baseURL := "https://metrics.example.com/api/v1/?extra_filters%5B%5D=%7Bvm_account_id%3D%22400%22%7D"

	got, err := resolver.buildMetricsURL(context.Background(), pool, baseURL)
	require.NoError(t, err)
	gotURL, err := url.Parse(got)
	require.NoError(t, err)
	require.Contains(t, gotURL.Query()["extra_filters[]"], `{vm_account_id=~"^(400|421)$"}`)
	require.Equal(t, 1, fake.calls[logicalCluster.ClusterID])
}
