package group

import (
	"context"
	"encoding/csv"
	"fmt"
	"strings"
	"testing"
	"time"

	"github.com/DATA-DOG/go-sqlmock"
	modelgroup "github.com/perfect-panel/server/internal/model/group"
	"github.com/perfect-panel/server/internal/svc"
	"github.com/perfect-panel/server/internal/types"
	"github.com/perfect-panel/server/pkg/logger"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

func TestGetGroupHistoryDetailReadsConfigFromSystem(t *testing.T) {
	db, mock, cleanup := newGroupTestDB(t)
	defer cleanup()

	now := time.Unix(1710000000, 0)
	mock.ExpectQuery("FROM `group_history`").
		WithArgs(int64(9), 1).
		WillReturnRows(sqlmock.NewRows([]string{
			"id", "group_mode", "trigger_type", "state", "total_users", "success_count", "failed_count", "start_time", "end_time", "error_message", "created_at",
		}).AddRow(int64(9), "traffic", "manual", "completed", 1, 1, 0, now, now, "", now))
	mock.ExpectQuery("FROM `group_history_detail`").
		WithArgs(int64(9)).
		WillReturnRows(sqlmock.NewRows([]string{
			"id", "history_id", "node_group_id", "user_count", "node_count", "user_data", "created_at",
		}).AddRow(int64(1), int64(9), int64(3), 1, 2, `[{"id":7,"email":"u@example.com"}]`, now))
	mock.ExpectQuery("FROM `system`").
		WithArgs("group", "traffic_config").
		WillReturnRows(sqlmock.NewRows([]string{"value"}).AddRow(`{"strategy":"closed"}`))

	logic := newTestGroupHistoryDetailLogic(db)
	resp, err := logic.GetGroupHistoryDetail(&types.GetGroupHistoryDetailRequest{Id: 9})
	if err != nil {
		t.Fatalf("GetGroupHistoryDetail error: %v", err)
	}
	if got := resp.ConfigSnapshot["config"].(map[string]interface{})["strategy"]; got != "closed" {
		t.Fatalf("config snapshot strategy = %v, want closed", got)
	}
	assertGroupExpectations(t, mock)
}

func TestExportGroupResultParsesHistoryUserDataJSON(t *testing.T) {
	db, mock, cleanup := newGroupTestDB(t)
	defer cleanup()

	historyID := int64(11)
	now := time.Unix(1710000000, 0)
	mock.ExpectQuery("FROM `group_history_detail`").
		WithArgs(historyID).
		WillReturnRows(sqlmock.NewRows([]string{
			"id", "history_id", "node_group_id", "user_count", "node_count", "user_data", "created_at",
		}).AddRow(int64(1), historyID, int64(8), 1, 2, `[{"id":42,"email":"u@example.com"}]`, now))
	mock.ExpectQuery("FROM `node_group`").
		WithArgs(int64(8), 1).
		WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(int64(8), "VIP"))

	logic := newTestExportGroupResultLogic(db)
	data, filename, err := logic.ExportGroupResult(&types.ExportGroupResultRequest{HistoryId: &historyID})
	if err != nil {
		t.Fatalf("ExportGroupResult error: %v", err)
	}
	if filename != "group_result_11.csv" {
		t.Fatalf("filename = %q, want group_result_11.csv", filename)
	}

	records, err := csv.NewReader(strings.NewReader(strings.TrimPrefix(string(data), "\ufeff"))).ReadAll()
	if err != nil {
		t.Fatalf("read csv: %v", err)
	}
	if len(records) != 2 || strings.Join(records[1], ",") != "42,8,VIP" {
		t.Fatalf("csv records = %#v, want user/group row", records)
	}
	assertGroupExpectations(t, mock)
}

func TestExportGroupResultRejectsInvalidHistoryUserDataJSON(t *testing.T) {
	db, mock, cleanup := newGroupTestDB(t)
	defer cleanup()

	historyID := int64(12)
	now := time.Unix(1710000000, 0)
	mock.ExpectQuery("FROM `group_history_detail`").
		WithArgs(historyID).
		WillReturnRows(sqlmock.NewRows([]string{
			"id", "history_id", "node_group_id", "user_count", "node_count", "user_data", "created_at",
		}).AddRow(int64(1), historyID, int64(8), 1, 2, `{bad-json`, now))

	logic := newTestExportGroupResultLogic(db)
	_, _, err := logic.ExportGroupResult(&types.ExportGroupResultRequest{HistoryId: &historyID})
	if err == nil || !strings.Contains(err.Error(), "parse group history user_data failed") {
		t.Fatalf("ExportGroupResult error = %v, want parse error", err)
	}
	assertGroupExpectations(t, mock)
}

func TestDeleteNodeGroupRejectsSubscribeReferences(t *testing.T) {
	tests := []struct {
		name                  string
		defaultSubscribeCount int64
		subscribeGroupCount   int64
		userSubscribeCount    int64
		wantErr               string
	}{
		{name: "subscribe default group", defaultSubscribeCount: 1, wantErr: "default node group"},
		{name: "subscribe group list", subscribeGroupCount: 1, wantErr: "node group list"},
		{name: "user subscribe group", userSubscribeCount: 1, wantErr: "user subscribes"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			db, mock, cleanup := newGroupTestDB(t)
			defer cleanup()

			expectDeleteNodeGroupBaseChecks(mock, 5)
			mock.ExpectQuery("FROM `subscribe`").
				WithArgs(int64(5)).
				WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(tt.defaultSubscribeCount))
			if tt.defaultSubscribeCount == 0 {
				mock.ExpectQuery("FROM `subscribe`").
					WithArgs("[5]").
					WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(tt.subscribeGroupCount))
			}
			if tt.defaultSubscribeCount == 0 && tt.subscribeGroupCount == 0 {
				mock.ExpectQuery("FROM `user_subscribe`").
					WithArgs(int64(5)).
					WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(tt.userSubscribeCount))
			}

			logic := newTestDeleteNodeGroupLogic(db)
			err := logic.DeleteNodeGroup(&types.DeleteNodeGroupRequest{Id: 5})
			if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
				t.Fatalf("DeleteNodeGroup error = %v, want contains %q", err, tt.wantErr)
			}
			assertGroupExpectations(t, mock)
		})
	}
}

func TestResetGroupsRollsBackOnFailure(t *testing.T) {
	db, mock, cleanup := newGroupTestDB(t)
	defer cleanup()

	mock.ExpectBegin()
	mock.ExpectExec("DELETE FROM `node_group`").WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectExec("UPDATE `subscribe`").
		WithArgs(int64(0), "[]").
		WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectExec("UPDATE `nodes`").
		WithArgs("[]").
		WillReturnError(fmt.Errorf("node update failed"))
	mock.ExpectRollback()

	logic := newTestResetGroupsLogic(db)
	err := logic.ResetGroups()
	if err == nil || !strings.Contains(err.Error(), "node update failed") {
		t.Fatalf("ResetGroups error = %v, want node update failure", err)
	}
	assertGroupExpectations(t, mock)
}

func TestPreviewUserNodesIncludesPublicNodesInGroupMode(t *testing.T) {
	db, mock, cleanup := newGroupTestDB(t)
	defer cleanup()

	now := time.Unix(1710000000, 0)
	mock.ExpectQuery("FROM `user_subscribe`").
		WithArgs(int64(100), int8(0), int8(1)).
		WillReturnRows(sqlmock.NewRows([]string{"id", "user_id", "subscribe_id", "node_group_id"}).
			AddRow(int64(1), int64(100), int64(10), int64(5)))
	mock.ExpectQuery("FROM `subscribe`").
		WithArgs(int64(10)).
		WillReturnRows(sqlmock.NewRows([]string{"id", "node_group_id", "node_group_ids", "nodes", "node_tags"}).
			AddRow(int64(10), int64(0), "[]", "", ""))
	mock.ExpectQuery("FROM `system`").
		WithArgs("group", "enabled").
		WillReturnRows(sqlmock.NewRows([]string{"value"}).AddRow("true"))
	mock.ExpectQuery("FROM `nodes`").
		WithArgs(true).
		WillReturnRows(sqlmock.NewRows([]string{
			"id", "name", "tags", "port", "address", "server_id", "protocol", "enabled", "sort", "node_group_ids", "created_at", "updated_at",
		}).
			AddRow(int64(1), "group-node", "", uint16(443), "g.example.com", int64(1), "vless", true, 1, "[5]", now, now).
			AddRow(int64(2), "public-node", "", uint16(443), "p.example.com", int64(1), "vless", true, 2, "[]", now, now))
	mock.ExpectQuery("FROM `node_group`").
		WithArgs(int64(5)).
		WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(int64(5), "Group A"))

	logic := newTestPreviewUserNodesLogic(db)
	resp, err := logic.PreviewUserNodes(&types.PreviewUserNodesRequest{UserId: 100})
	if err != nil {
		t.Fatalf("PreviewUserNodes error: %v", err)
	}
	if !hasNodeGroup(resp.NodeGroups, 0, "public-node") {
		t.Fatalf("PreviewUserNodes node groups = %#v, want public node group", resp.NodeGroups)
	}
	assertGroupExpectations(t, mock)
}

func TestTrafficRangeMatchingUsesClosedBounds(t *testing.T) {
	min0, max10 := int64(0), int64(10)
	min10, max20 := int64(10), int64(20)
	nodeGroups := []modelgroup.NodeGroup{
		{Id: 1, MinTrafficGB: &min0, MaxTrafficGB: &max10},
		{Id: 2, MinTrafficGB: &min10, MaxTrafficGB: &max20},
	}

	tests := map[float64]int64{
		0:  1,
		10: 1,
		15: 2,
		21: 0,
	}
	for used, want := range tests {
		if got := matchTrafficNodeGroup(used, nodeGroups); got != want {
			t.Fatalf("matchTrafficNodeGroup(%v) = %d, want %d", used, got, want)
		}
	}
}

func TestNormalizePaginationDefaultsAndMax(t *testing.T) {
	tests := []struct {
		page, size int
		wantPage   int
		wantSize   int
	}{
		{page: 0, size: 0, wantPage: 1, wantSize: defaultPageSize},
		{page: -1, size: -5, wantPage: 1, wantSize: defaultPageSize},
		{page: 2, size: maxPageSize + 1, wantPage: 2, wantSize: maxPageSize},
	}

	for _, tt := range tests {
		gotPage, gotSize := normalizePagination(tt.page, tt.size)
		if gotPage != tt.wantPage || gotSize != tt.wantSize {
			t.Fatalf("normalizePagination(%d, %d) = (%d, %d), want (%d, %d)", tt.page, tt.size, gotPage, gotSize, tt.wantPage, tt.wantSize)
		}
	}
}

func newGroupTestDB(t *testing.T) (*gorm.DB, sqlmock.Sqlmock, func()) {
	t.Helper()

	sqlDB, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherFunc(func(expectedSQL, actualSQL string) error {
		if strings.Contains(actualSQL, expectedSQL) {
			return nil
		}
		return fmt.Errorf("actual sql %q does not contain %q", actualSQL, expectedSQL)
	})))
	if err != nil {
		t.Fatalf("create sqlmock: %v", err)
	}

	db, err := gorm.Open(mysql.New(mysql.Config{Conn: sqlDB, SkipInitializeWithVersion: true}), &gorm.Config{})
	if err != nil {
		_ = sqlDB.Close()
		t.Fatalf("open gorm db: %v", err)
	}

	return db, mock, func() {
		_ = sqlDB.Close()
	}
}

func newTestGroupHistoryDetailLogic(db *gorm.DB) *GetGroupHistoryDetailLogic {
	ctx := context.Background()
	return &GetGroupHistoryDetailLogic{
		Logger: logger.WithContext(ctx),
		ctx:    ctx,
		svcCtx: &svc.ServiceContext{DB: db},
	}
}

func newTestExportGroupResultLogic(db *gorm.DB) *ExportGroupResultLogic {
	ctx := context.Background()
	return &ExportGroupResultLogic{
		Logger: logger.WithContext(ctx),
		ctx:    ctx,
		svcCtx: &svc.ServiceContext{DB: db},
	}
}

func newTestDeleteNodeGroupLogic(db *gorm.DB) *DeleteNodeGroupLogic {
	ctx := context.Background()
	return &DeleteNodeGroupLogic{
		Logger: logger.WithContext(ctx),
		ctx:    ctx,
		svcCtx: &svc.ServiceContext{DB: db},
	}
}

func newTestResetGroupsLogic(db *gorm.DB) *ResetGroupsLogic {
	ctx := context.Background()
	return &ResetGroupsLogic{
		Logger: logger.WithContext(ctx),
		ctx:    ctx,
		svcCtx: &svc.ServiceContext{DB: db},
	}
}

func newTestPreviewUserNodesLogic(db *gorm.DB) *PreviewUserNodesLogic {
	ctx := context.Background()
	return &PreviewUserNodesLogic{
		Logger: logger.WithContext(ctx),
		ctx:    ctx,
		svcCtx: &svc.ServiceContext{DB: db},
	}
}

func expectDeleteNodeGroupBaseChecks(mock sqlmock.Sqlmock, nodeGroupId int64) {
	now := time.Unix(1710000000, 0)
	mock.ExpectQuery("FROM `node_group`").
		WithArgs(nodeGroupId, 1).
		WillReturnRows(sqlmock.NewRows([]string{"id", "name", "created_at", "updated_at"}).
			AddRow(nodeGroupId, "Group", now, now))
	mock.ExpectQuery("FROM `nodes`").
		WithArgs(fmt.Sprintf("[%d]", nodeGroupId)).
		WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(0))
}

func hasNodeGroup(items []types.NodeGroupItem, id int64, nodeName string) bool {
	for _, item := range items {
		if item.Id != id {
			continue
		}
		for _, n := range item.Nodes {
			if n.Name == nodeName {
				return true
			}
		}
	}
	return false
}

func assertGroupExpectations(t *testing.T, mock sqlmock.Sqlmock) {
	t.Helper()
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("unmet sql expectations: %v", err)
	}
}
