package service

import (
	"strings"
	"testing"

	"github.com/mhsanaei/3x-ui/v3/internal/database"
	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
)

func TestUpdate_PersistsFields_NoInbound(t *testing.T) {
	cases := []struct {
		name     string
		mutate   func(c *model.Client)
		readBack func(rec *model.ClientRecord) any
		want     any
	}{
		{
			name:     "subId",
			mutate:   func(c *model.Client) { c.SubID = "new-sub-id" },
			readBack: func(rec *model.ClientRecord) any { return rec.SubID },
			want:     "new-sub-id",
		},
		{
			name:     "totalGB cleared to zero",
			mutate:   func(c *model.Client) { c.TotalGB = 0 },
			readBack: func(rec *model.ClientRecord) any { return rec.TotalGB },
			want:     int64(0),
		},
		{
			name:     "expiryTime",
			mutate:   func(c *model.Client) { c.ExpiryTime = 1700000000 },
			readBack: func(rec *model.ClientRecord) any { return rec.ExpiryTime },
			want:     int64(1700000000),
		},
		{
			name:     "limitIp",
			mutate:   func(c *model.Client) { c.LimitIP = 7 },
			readBack: func(rec *model.ClientRecord) any { return rec.LimitIP },
			want:     7,
		},
		{
			name:     "tgId",
			mutate:   func(c *model.Client) { c.TgID = 9876543210 },
			readBack: func(rec *model.ClientRecord) any { return rec.TgID },
			want:     int64(9876543210),
		},
		{
			name:     "comment cleared to empty",
			mutate:   func(c *model.Client) { c.Comment = "" },
			readBack: func(rec *model.ClientRecord) any { return rec.Comment },
			want:     "",
		},
		{
			name:     "reset",
			mutate:   func(c *model.Client) { c.Reset = 30 },
			readBack: func(rec *model.ClientRecord) any { return rec.Reset },
			want:     30,
		},
		{
			name:     "flow",
			mutate:   func(c *model.Client) { c.Flow = "xtls-rprx-vision" },
			readBack: func(rec *model.ClientRecord) any { return rec.Flow },
			want:     "xtls-rprx-vision",
		},
		{
			name:     "security",
			mutate:   func(c *model.Client) { c.Security = "aes-128-gcm" },
			readBack: func(rec *model.ClientRecord) any { return rec.Security },
			want:     "aes-128-gcm",
		},
		{
			name:     "uuid rotated",
			mutate:   func(c *model.Client) { c.ID = "22222222-2222-2222-2222-222222222222" },
			readBack: func(rec *model.ClientRecord) any { return rec.UUID },
			want:     "22222222-2222-2222-2222-222222222222",
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			setupBulkDB(t)
			svc := &ClientService{}
			inboundSvc := &InboundService{}

			email := "noib-" + strings.ReplaceAll(strings.ToLower(tc.name), " ", "-") + "@x"
			rec := &model.ClientRecord{
				Email:      email,
				UUID:       "11111111-1111-1111-1111-111111111111",
				SubID:      email,
				TotalGB:    5,
				ExpiryTime: 1000,
				LimitIP:    1,
				TgID:       1,
				Comment:    "seeded",
				Reset:      1,
				Flow:       "seeded-flow",
				Security:   "seeded-sec",
			}
			if err := database.GetDB().Create(rec).Error; err != nil {
				t.Fatalf("create record: %v", err)
			}

			updated := rec.ToClient()
			tc.mutate(updated)
			if _, err := svc.Update(inboundSvc, rec.Id, *updated, rec.LimitHwid); err != nil {
				t.Fatalf("Update: %v", err)
			}

			got, err := svc.GetByID(rec.Id)
			if err != nil {
				t.Fatalf("GetByID: %v", err)
			}
			if tc.readBack(got) != tc.want {
				t.Fatalf("%s: not persisted for no-inbound client, got %v, want %v", tc.name, tc.readBack(got), tc.want)
			}
		})
	}
}

func TestUpdate_NoInbound_PreservesCredentialsWhenOmitted(t *testing.T) {
	setupBulkDB(t)
	svc := &ClientService{}
	inboundSvc := &InboundService{}

	email := "noib-preserve@x"
	rec := &model.ClientRecord{
		Email:    email,
		UUID:     "11111111-1111-1111-1111-111111111111",
		SubID:    email,
		Password: "seeded-pw",
		Auth:     "seeded-auth",
		Secret:   "seeded-secret",
	}
	if err := database.GetDB().Create(rec).Error; err != nil {
		t.Fatalf("create record: %v", err)
	}

	updated := rec.ToClient()
	updated.ID = ""
	updated.Password = ""
	updated.Auth = ""
	updated.Secret = ""
	updated.Comment = "only comment changed"
	if _, err := svc.Update(inboundSvc, rec.Id, *updated, rec.LimitHwid); err != nil {
		t.Fatalf("Update: %v", err)
	}

	got, err := svc.GetByID(rec.Id)
	if err != nil {
		t.Fatalf("GetByID: %v", err)
	}
	if got.UUID != "11111111-1111-1111-1111-111111111111" {
		t.Fatalf("uuid wiped on partial update, got %q", got.UUID)
	}
	if got.Password != "seeded-pw" {
		t.Fatalf("password wiped on partial update, got %q", got.Password)
	}
	if got.Auth != "seeded-auth" {
		t.Fatalf("auth wiped on partial update, got %q", got.Auth)
	}
	if got.Secret != "seeded-secret" {
		t.Fatalf("secret wiped on partial update, got %q", got.Secret)
	}
	if got.Comment != "only comment changed" {
		t.Fatalf("comment not persisted, got %q", got.Comment)
	}
}

func TestApplyClientRecordMerge_MirrorsSyncInboundRules(t *testing.T) {
	row := &model.ClientRecord{
		UUID:     "kept-uuid",
		Password: "kept-pw",
		Flow:     "kept-flow",
		TotalGB:  9,
		Group:    "kept-group",
		Comment:  "kept-comment",
	}
	incoming := &model.ClientRecord{
		Password: "new-pw",
		TotalGB:  0,
		Comment:  "new-comment",
	}

	applyClientRecordMerge(row, incoming)

	if row.UUID != "kept-uuid" {
		t.Fatalf("empty incoming UUID should preserve stored UUID, got %q", row.UUID)
	}
	if row.Password != "new-pw" {
		t.Fatalf("non-empty incoming Password should overwrite, got %q", row.Password)
	}
	if row.Flow != "" {
		t.Fatalf("incoming Flow is unconditional and should overwrite with empty, got %q", row.Flow)
	}
	if row.TotalGB != 0 {
		t.Fatalf("incoming TotalGB is unconditional and should overwrite with zero, got %v", row.TotalGB)
	}
	if row.Group != "kept-group" {
		t.Fatalf("empty incoming Group should preserve stored group, got %q", row.Group)
	}
	if row.Comment != "new-comment" {
		t.Fatalf("incoming Comment is unconditional and should overwrite, got %q", row.Comment)
	}
}
