package clusters

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

	"github.com/DATA-DOG/go-sqlmock"
	logr "github.com/go-logr/logr/testing"
	. "github.com/onsi/gomega"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/stretchr/testify/require"
	observabilitypingcapcomv1 "github.com/tidbcloud/observability/api/regionalserver/resourceworker/v1"
	"github.com/tidbcloud/observability/pkg/common/constants"
	"github.com/tidbcloud/observability/pkg/common/structs"
	"github.com/tidbcloud/observability/pkg/resourceworker/controllers/fake"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

func TestMetricsNewandDescribe(t *testing.T) {
	g := NewGomegaWithT(t)
	serviceInfo := structs.ServiceInfo{
		Region: "us-sest-2",
		Vendor: "aws",
	}
	c := NewClusterCollector(&fake.MockClient{}, nil, logr.NewTestLogger(t), serviceInfo)
	ch := make(chan *prometheus.Desc, 1)
	c.Describe(ch)
	g.Expect(<-ch).Should(Equal(ClusterInfo))
}

func TestMetricsCollector(t *testing.T) {
	g := NewGomegaWithT(t)
	serviceInfo := structs.ServiceInfo{
		Region:            "us-west-2",
		LogicalRegionName: "us-west-2-lr1",
		Vendor:            "aws",
	}
	cli := &fake.MockClient{
		MockList: fake.MockListFn(func(ctx context.Context, list runtime.Object, opts ...client.ListOption) error {
			li := list.(*observabilitypingcapcomv1.ClusterList)
			labels := make(map[string]string)
			labels[constants.LabelTenantID] = "tenantid"
			labels[constants.LabelProjectID] = "projectid"
			labels[constants.LabelApplicationID] = "applicationid"
			labels[constants.LabelBizType] = "biztype"
			labels[constants.LabelShareMode] = "shared"
			li.Items = append(li.Items, observabilitypingcapcomv1.Cluster{
				Spec: observabilitypingcapcomv1.ClusterSpec{
					Prometheus: observabilitypingcapcomv1.Prometheus{
						Version:          "v1",
						AvailabilityZone: "az1",
					},
				},
				ObjectMeta: metav1.ObjectMeta{
					Labels: labels,
					Name:   "test1",
				},
				Status: observabilitypingcapcomv1.ClusterStatus{
					Phase: constants.ACTIVE,
				},
			})
			return nil
		}),
	}
	c := NewClusterCollector(cli, nil, logr.NewTestLogger(t), serviceInfo)
	ch := make(chan prometheus.Metric, 1)
	c.Collect(ch)

	// Just verify we got a metric, don't check exact equality due to label ordering
	metric := <-ch
	g.Expect(metric).ShouldNot(BeNil())
}

func TestMetricsCollectorWithRDS(t *testing.T) {
	g := NewGomegaWithT(t)

	// Create mock RDS client
	sqlDB, mock, gormDB := setupMockDB(t)
	defer sqlDB.Close()

	serviceInfo := structs.ServiceInfo{
		Region:            "us-west-2",
		LogicalRegionName: "us-west-2-lr1",
		Vendor:            "aws",
		ControllerConfig: structs.ControllerConfig{
			ServerlessConfig: structs.ServerlessControllerConfig{
				EnableRDSClient: true,
			},
		},
	}

	// Mock K8s client
	cli := &fake.MockClient{
		MockList: fake.MockListFn(func(ctx context.Context, list runtime.Object, opts ...client.ListOption) error {
			li := list.(*observabilitypingcapcomv1.ClusterList)
			labels := make(map[string]string)
			labels[constants.LabelTenantID] = "tenant-k8s"
			labels[constants.LabelProjectID] = "project-k8s"
			labels[constants.LabelApplicationID] = "app-k8s"
			labels[constants.LabelBizType] = "dedicated"
			labels[constants.LabelShareMode] = "shared"
			li.Items = append(li.Items, observabilitypingcapcomv1.Cluster{
				Spec: observabilitypingcapcomv1.ClusterSpec{
					Prometheus: observabilitypingcapcomv1.Prometheus{
						Version:          "v1",
						AvailabilityZone: "az1",
					},
				},
				ObjectMeta: metav1.ObjectMeta{
					Labels: labels,
					Name:   "k8s-cluster",
				},
				Status: observabilitypingcapcomv1.ClusterStatus{
					Phase: constants.ACTIVE,
				},
			})
			return nil
		}),
	}

	// Mock RDS client
	rdsCli := newMockRDSClient(gormDB)

	// Mock biz_type count query
	bizTypeRows := sqlmock.NewRows([]string{"biz_type", "count"}).
		AddRow("dedicated", 2).
		AddRow("serverless", 3)
	mock.ExpectQuery("SELECT biz_type, COUNT\\(\\*\\) as count FROM `clusters` GROUP BY `biz_type`").
		WillReturnRows(bizTypeRows)

	// Mock serverless parent clusters query
	clusterRows := sqlmock.NewRows([]string{
		"cluster_id", "namespace", "region", "biz_type", "tenant", "project_id", "application_id",
		"deletion_timestamp", "creation_timestamp", "update_timestamp",
	}).
		AddRow("serverless-parent-1", "default", "us-west-2", "serverless", "tenant-1", "project-1",
			constants.ApplicationIDServerless, nil, time.Now(), time.Now()).
		AddRow("serverless-parent-2", "default", "us-west-2", "serverless", "tenant-2", "project-2",
			constants.ApplicationIDServerless, nil, time.Now(), time.Now())

	mock.ExpectQuery("SELECT \\* FROM `clusters`").
		WithArgs(constants.ApplicationIDServerless).
		WillReturnRows(clusterRows)

	// Mock Spec preload
	specRows := sqlmock.NewRows([]string{
		"cluster_id", "labels", "annotations", "endpoint_service_name", "endpoint_service_user_arn",
		"endpoint_service_shared", "mute_alert", "vm_account_id", "vm_project_id", "vm_support_serverless",
		"vm_alert_max_queue_size", "vm_cluster_name", "vm_availability_zone", "vm_rule_selector",
		"vm_alert_cpu_request", "vm_alert_cpu_limit", "vm_alert_memory_request", "vm_alert_memory_limit",
		"vm_alert_managers", "creation_timestamp", "update_timestamp",
	}).
		AddRow("serverless-parent-1", `{"observability.pingcap.com/tenantID":"tenant-1","observability.pingcap.com/projectID":"project-1","observability.pingcap.com/share-mode":"shared"}`,
			`{}`, "endpoint-1", "arn", false, false, 100, 200, true, 0, "vm-1", "az1", `{}`,
			"", "", "", "", `[]`, time.Now(), time.Now()).
		AddRow("serverless-parent-2", `{"observability.pingcap.com/tenantID":"tenant-2","observability.pingcap.com/projectID":"project-2","observability.pingcap.com/share-mode":"shared"}`,
			`{}`, "endpoint-2", "arn", false, false, 101, 201, true, 0, "vm-2", "az1", `{}`,
			"", "", "", "", `[]`, time.Now(), time.Now())

	mock.ExpectQuery("SELECT \\* FROM `cluster_specs` WHERE `cluster_specs`.`cluster_id` IN \\(\\?,\\?\\)").
		WithArgs("serverless-parent-1", "serverless-parent-2").
		WillReturnRows(specRows)

	// Mock Status preload
	statusRows := sqlmock.NewRows([]string{
		"cluster_id", "phase", "service_name", "dns_name", "update_at", "resource_version",
		"creation_timestamp", "update_timestamp",
	}).
		AddRow("serverless-parent-1", "Running", "svc-1", "dns-1", "2024-01-01", 1, time.Now(), time.Now()).
		AddRow("serverless-parent-2", "Running", "svc-2", "dns-2", "2024-01-01", 1, time.Now(), time.Now())

	mock.ExpectQuery("SELECT \\* FROM `cluster_statuses` WHERE `cluster_statuses`.`cluster_id` IN \\(\\?,\\?\\)").
		WithArgs("serverless-parent-1", "serverless-parent-2").
		WillReturnRows(statusRows)

	c := NewClusterCollector(cli, rdsCli, logr.NewTestLogger(t), serviceInfo)
	ch := make(chan prometheus.Metric, 10)
	c.Collect(ch)

	// Verify we got metrics from both K8s and RDS
	metricsCount := 0
	for len(ch) > 0 {
		<-ch
		metricsCount++
	}

	// Should have 1 from K8s + 2 from RDS = 3 cluster info metrics
	g.Expect(metricsCount).Should(Equal(3))
	g.Expect(mock.ExpectationsWereMet()).Should(Succeed())
}

// setupMockDB creates a mock database and gorm.DB instance for testing
func setupMockDB(t *testing.T) (*sql.DB, sqlmock.Sqlmock, *gorm.DB) {
	sqlDB, mock, err := sqlmock.New()
	require.NoError(t, err)

	gormDB, err := gorm.Open(mysql.New(mysql.Config{
		Conn:                      sqlDB,
		SkipInitializeWithVersion: true,
	}), &gorm.Config{
		Logger: logger.Default.LogMode(logger.Silent),
	})
	require.NoError(t, err)

	return sqlDB, mock, gormDB
}

// mockRDSClient wraps the fake client with RawClient support for testing
type mockRDSClient struct {
	client.Client
	db *gorm.DB
}

func newMockRDSClient(db *gorm.DB) *mockRDSClient {
	return &mockRDSClient{
		Client: &fake.MockClient{},
		db:     db,
	}
}

func (m *mockRDSClient) RawClient() *gorm.DB {
	return m.db
}
