package service

import (
	"context"
	"errors"
	"testing"

	"github.com/glebarez/sqlite"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"

	"oci-portal/internal/crypto"
	"oci-portal/internal/model"
	"oci-portal/internal/oci"
)

const testPEM = "-----BEGIN PRIVATE KEY-----\nfake\n-----END PRIVATE KEY-----"

// fakeClient 是 oci.Client 的测试替身，普通测试不访问真实 OCI。
// 内嵌接口以自动满足 oci.Client，未覆写的方法不会被这些测试调用。
type fakeClient struct {
	oci.Client

	tenancy    oci.TenancyInfo
	tenancyErr error
	profile    oci.AccountProfile
	profileErr error
	caps       oci.AccountCapabilities
	capsErr    error

	regionSubs        []oci.RegionSubscription
	regionSubsErr     error
	regionSubsCalls   int
	compartments      []oci.Compartment
	compartmentsErr   error
	compartmentsCalls int
	subscribeErr      error
	limitValues       []oci.LimitValue
	limitsErr         error
	limitServices     []oci.LimitService
	subscriptions     []oci.SubscriptionInfo
	subDetail         oci.SubscriptionDetail
	subDetailErr      error
	instances         []oci.Instance
	instancesErr      error
	costItems         []oci.CostItem
	costErr           error
	instance          oci.Instance
	instanceErr       error
	image             oci.Image
	imageErr          error

	subscribedHomeRegion string
	subscribedKey        string
	limitsQuery          oci.LimitsQuery
	subDetailID          string
	launched             []oci.CreateInstanceInput
}

func (f *fakeClient) ValidateKey(ctx context.Context, cred oci.Credentials) (oci.TenancyInfo, error) {
	return f.tenancy, f.tenancyErr
}

func (f *fakeClient) GetAccountCapabilities(ctx context.Context, cred oci.Credentials, region string) (oci.AccountCapabilities, error) {
	return f.caps, f.capsErr
}

func (f *fakeClient) FetchAccountProfile(ctx context.Context, cred oci.Credentials) (oci.AccountProfile, error) {
	return f.profile, f.profileErr
}

func (f *fakeClient) ListRegionSubscriptions(ctx context.Context, cred oci.Credentials) ([]oci.RegionSubscription, error) {
	f.regionSubsCalls++
	return f.regionSubs, f.regionSubsErr
}

func (f *fakeClient) ListCompartments(ctx context.Context, cred oci.Credentials) ([]oci.Compartment, error) {
	f.compartmentsCalls++
	return f.compartments, f.compartmentsErr
}

func (f *fakeClient) SubscribeRegion(ctx context.Context, cred oci.Credentials, homeRegion, regionKey string) error {
	f.subscribedHomeRegion = homeRegion
	f.subscribedKey = regionKey
	return f.subscribeErr
}

func (f *fakeClient) ListLimits(ctx context.Context, cred oci.Credentials, q oci.LimitsQuery) ([]oci.LimitValue, error) {
	f.limitsQuery = q
	return f.limitValues, f.limitsErr
}

func (f *fakeClient) ListLimitServices(ctx context.Context, cred oci.Credentials, region string) ([]oci.LimitService, error) {
	return f.limitServices, nil
}

func (f *fakeClient) ListSubscriptions(ctx context.Context, cred oci.Credentials) ([]oci.SubscriptionInfo, error) {
	return f.subscriptions, nil
}

func (f *fakeClient) GetSubscription(ctx context.Context, cred oci.Credentials, subscriptionID string) (oci.SubscriptionDetail, error) {
	f.subDetailID = subscriptionID
	return f.subDetail, f.subDetailErr
}

func (f *fakeClient) LaunchInstance(ctx context.Context, cred oci.Credentials, in oci.CreateInstanceInput) (oci.Instance, error) {
	f.launched = append(f.launched, in)
	return oci.Instance{DisplayName: in.DisplayName, LifecycleState: "PROVISIONING", FreeformTags: in.FreeformTags}, nil
}

// ListAvailabilityDomains 默认单可用域,抢机自动轮询退化为固定 ad-1。
func (f *fakeClient) ListAvailabilityDomains(ctx context.Context, cred oci.Credentials, region string) ([]string, error) {
	return []string{"fake-ad-1"}, nil
}

func (f *fakeClient) ListInstances(ctx context.Context, cred oci.Credentials, region string) ([]oci.Instance, error) {
	return f.instances, f.instancesErr
}

func (f *fakeClient) GetInstance(ctx context.Context, cred oci.Credentials, region, instanceID string) (oci.Instance, error) {
	return f.instance, f.instanceErr
}

func (f *fakeClient) GetImage(ctx context.Context, cred oci.Credentials, region, imageID string) (oci.Image, error) {
	return f.image, f.imageErr
}

func (f *fakeClient) SummarizeCosts(ctx context.Context, cred oci.Credentials, q oci.CostQuery) ([]oci.CostItem, error) {
	return f.costItems, f.costErr
}

func newTestService(t *testing.T, client oci.Client) *OciConfigService {
	t.Helper()
	db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
		Logger: logger.Default.LogMode(logger.Silent),
	})
	if err != nil {
		t.Fatalf("open in-memory sqlite: %v", err)
	}
	// :memory: 库每条连接各自独立,后台 goroutine(模型验证等)触发第二条连接会看到空库;
	// 收敛到单连接,与 tenantdelete 测试环境一致
	sqlDB, err := db.DB()
	if err != nil {
		t.Fatalf("database handle: %v", err)
	}
	sqlDB.SetMaxOpenConns(1)
	if err := db.AutoMigrate(
		&model.OciConfig{}, &model.RegionCache{}, &model.CompartmentCache{}, &model.Proxy{},
	); err != nil {
		t.Fatalf("auto migrate: %v", err)
	}
	cipher, err := crypto.NewCipher("test-data-key")
	if err != nil {
		t.Fatalf("new cipher: %v", err)
	}
	return NewOciConfigService(db, cipher, client)
}

func trialImportInput() ImportInput {
	return ImportInput{
		Alias:       "试用期",
		Group:       "教育",
		TenancyOCID: "ocid1.tenancy.oc1..t",
		UserOCID:    "ocid1.user.oc1..u",
		Region:      "eu-frankfurt-1",
		Fingerprint: "aa:bb",
		PrivateKey:  testPEM,
	}
}

func TestImportAutoVerifiesAndClassifies(t *testing.T) {
	client := &fakeClient{
		tenancy: oci.TenancyInfo{Name: "mytenancy", HomeRegionKey: "FRA"},
		profile: oci.AccountProfile{
			AccountType:      model.AccountTypeTrial,
			SubscriptionID:   "ocid1.organizationssubscription.oc1..s",
			PaymentModel:     "FREE_TRIAL",
			SubscriptionTier: "FREE_AND_TRIAL",
			PromotionStatus:  "ACTIVE",
			PromotionAmount:  300,
		},
	}
	svc := newTestService(t, client)

	cfg, err := svc.Import(context.Background(), trialImportInput())
	if err != nil {
		t.Fatalf("Import: %v", err)
	}
	if got, want := cfg.AliveStatus, model.AliveStatusAlive; got != want {
		t.Errorf("AliveStatus = %q, want %q", got, want)
	}
	if got, want := cfg.AccountType, model.AccountTypeTrial; got != want {
		t.Errorf("AccountType = %q, want %q", got, want)
	}
	if got, want := cfg.TenancyName, "mytenancy"; got != want {
		t.Errorf("TenancyName = %q, want %q", got, want)
	}
	if got, want := cfg.Group, "教育"; got != want {
		t.Errorf("Group = %q, want %q", got, want)
	}
	if cfg.LastVerifiedAt == nil {
		t.Error("LastVerifiedAt = nil, want set")
	}
	if cfg.PrivateKeyEnc == testPEM || cfg.PrivateKeyEnc == "" {
		t.Error("PrivateKeyEnc: private key stored without encryption")
	}
}

func TestImportDeadKeyStillSaved(t *testing.T) {
	client := &fakeClient{tenancyErr: errors.New("NotAuthenticated")}
	svc := newTestService(t, client)

	cfg, err := svc.Import(context.Background(), trialImportInput())
	if err != nil {
		t.Fatalf("Import: %v", err)
	}
	if got, want := cfg.AliveStatus, model.AliveStatusDead; got != want {
		t.Errorf("AliveStatus = %q, want %q", got, want)
	}
	if cfg.LastError == "" {
		t.Error("LastError is empty, want failure message")
	}
}

func TestImportParsesConfigINI(t *testing.T) {
	client := &fakeClient{profileErr: errors.New("skip profile")}
	svc := newTestService(t, client)

	cfg, err := svc.Import(context.Background(), ImportInput{
		Alias:      "ini 导入",
		ConfigINI:  "[DEFAULT]\nuser=ocid1.user.oc1..u\ntenancy=ocid1.tenancy.oc1..t\nfingerprint=ff:ee\nregion=ap-tokyo-1\n",
		PrivateKey: testPEM,
	})
	if err != nil {
		t.Fatalf("Import: %v", err)
	}
	if got, want := cfg.Region, "ap-tokyo-1"; got != want {
		t.Errorf("Region = %q, want %q", got, want)
	}
	if got, want := cfg.UserOCID, "ocid1.user.oc1..u"; got != want {
		t.Errorf("UserOCID = %q, want %q", got, want)
	}
}

func TestImportProfileFailureDegradesToUnknown(t *testing.T) {
	client := &fakeClient{
		tenancy:    oci.TenancyInfo{Name: "mytenancy"},
		profileErr: errors.New("subscription api forbidden"),
	}
	svc := newTestService(t, client)

	cfg, err := svc.Import(context.Background(), trialImportInput())
	if err != nil {
		t.Fatalf("Import: %v", err)
	}
	if got, want := cfg.AliveStatus, model.AliveStatusAlive; got != want {
		t.Errorf("AliveStatus = %q, want %q", got, want)
	}
	if got, want := cfg.AccountType, model.AccountTypeUnknown; got != want {
		t.Errorf("AccountType = %q, want %q", got, want)
	}
	if cfg.LastError == "" {
		t.Error("LastError is empty, want profile failure message")
	}
}

func TestImportRejectsInvalidInput(t *testing.T) {
	svc := newTestService(t, &fakeClient{})
	in := trialImportInput()
	in.PrivateKey = "not a pem"
	if _, err := svc.Import(context.Background(), in); err == nil {
		t.Error("Import with bad key: got nil error, want failure")
	}
}

func TestVerifyDetectsChanges(t *testing.T) {
	client := &fakeClient{
		tenancy: oci.TenancyInfo{Name: "mytenancy", HomeRegionKey: "FRA"},
		profile: oci.AccountProfile{
			AccountType:     model.AccountTypeTrial,
			PaymentModel:    "FREE_TRIAL",
			PromotionStatus: "ACTIVE",
		},
	}
	svc := newTestService(t, client)
	cfg, err := svc.Import(context.Background(), trialImportInput())
	if err != nil {
		t.Fatalf("Import: %v", err)
	}

	// 模拟云端状态变化：试用到期变免费
	client.profile = oci.AccountProfile{
		AccountType:     model.AccountTypeFree,
		PaymentModel:    "FREE_TRIAL",
		PromotionStatus: "EXPIRED",
	}
	updated, changes, err := svc.Verify(context.Background(), cfg.ID)
	if err != nil {
		t.Fatalf("Verify: %v", err)
	}
	if got, want := updated.AccountType, model.AccountTypeFree; got != want {
		t.Errorf("AccountType = %q, want %q", got, want)
	}
	if got, want := changes["accountType"], [2]string{model.AccountTypeTrial, model.AccountTypeFree}; got != want {
		t.Errorf("changes[accountType] = %v, want %v", got, want)
	}
	if got, want := changes["promotionStatus"], [2]string{"ACTIVE", "EXPIRED"}; got != want {
		t.Errorf("changes[promotionStatus] = %v, want %v", got, want)
	}
}

func TestVerifyNoChanges(t *testing.T) {
	client := &fakeClient{
		tenancy: oci.TenancyInfo{Name: "mytenancy", HomeRegionKey: "FRA"},
		profile: oci.AccountProfile{AccountType: model.AccountTypeTrial, PaymentModel: "FREE_TRIAL"},
	}
	svc := newTestService(t, client)
	cfg, err := svc.Import(context.Background(), trialImportInput())
	if err != nil {
		t.Fatalf("Import: %v", err)
	}
	_, changes, err := svc.Verify(context.Background(), cfg.ID)
	if err != nil {
		t.Fatalf("Verify: %v", err)
	}
	if len(changes) != 0 {
		t.Errorf("changes = %v, want empty", changes)
	}
}

func TestVerifyMissingConfig(t *testing.T) {
	svc := newTestService(t, &fakeClient{})
	if _, _, err := svc.Verify(context.Background(), 999); err == nil {
		t.Error("Verify(999): got nil error, want not found")
	}
}

func TestDelete(t *testing.T) {
	svc := newTestService(t, &fakeClient{tenancy: oci.TenancyInfo{Name: "t"}})
	migrateTenantDeleteModels(t, svc.db)
	cfg, err := svc.Import(context.Background(), trialImportInput())
	if err != nil {
		t.Fatalf("Import: %v", err)
	}
	if err := svc.Delete(context.Background(), cfg.ID); err != nil {
		t.Fatalf("Delete: %v", err)
	}
	if err := svc.Delete(context.Background(), cfg.ID); err == nil {
		t.Error("Delete twice: got nil error, want not found")
	}
}

func TestUpdateFields(t *testing.T) {
	const newPEM = "-----BEGIN PRIVATE KEY-----\nrotated\n-----END PRIVATE KEY-----"
	empty := ""
	prod := "生产"
	tests := []struct {
		name  string
		in    UpdateInput
		check func(t *testing.T, svc *OciConfigService, cfg *model.OciConfig)
	}{
		{
			name: "改别名与默认区域",
			in:   UpdateInput{Alias: "改名后", Region: "ap-tokyo-1"},
			check: func(t *testing.T, svc *OciConfigService, cfg *model.OciConfig) {
				if cfg.Alias != "改名后" || cfg.Region != "ap-tokyo-1" {
					t.Errorf("alias/region = %q/%q, want 改名后/ap-tokyo-1", cfg.Alias, cfg.Region)
				}
				if cred, err := svc.credentialsOf(cfg); err != nil || cred.PrivateKey != testPEM {
					t.Errorf("private key changed unexpectedly: %v", err)
				}
			},
		},
		{
			name: "改分组",
			in:   UpdateInput{Group: &prod},
			check: func(t *testing.T, _ *OciConfigService, cfg *model.OciConfig) {
				if cfg.Group != "生产" {
					t.Errorf("Group = %q, want 生产", cfg.Group)
				}
			},
		},
		{
			name: "清空分组",
			in:   UpdateInput{Group: &empty},
			check: func(t *testing.T, _ *OciConfigService, cfg *model.OciConfig) {
				if cfg.Group != "" {
					t.Errorf("Group = %q, want empty", cfg.Group)
				}
			},
		},
		{
			name: "缺省分组保持不变",
			in:   UpdateInput{Alias: "只改名"},
			check: func(t *testing.T, _ *OciConfigService, cfg *model.OciConfig) {
				if cfg.Group != "教育" {
					t.Errorf("Group = %q, want 教育 (unchanged)", cfg.Group)
				}
			},
		},
		{
			name: "换私钥与指纹",
			in:   UpdateInput{PrivateKey: newPEM, Fingerprint: "cc:dd"},
			check: func(t *testing.T, svc *OciConfigService, cfg *model.OciConfig) {
				if cfg.Fingerprint != "cc:dd" {
					t.Errorf("fingerprint = %q, want cc:dd", cfg.Fingerprint)
				}
				if cred, err := svc.credentialsOf(cfg); err != nil || cred.PrivateKey != newPEM {
					t.Errorf("private key not rotated: %v", err)
				}
			},
		},
		{
			name: "清除口令",
			in:   UpdateInput{Passphrase: &empty},
			check: func(t *testing.T, _ *OciConfigService, cfg *model.OciConfig) {
				if cfg.PassphraseEnc != "" {
					t.Errorf("PassphraseEnc = %q, want empty", cfg.PassphraseEnc)
				}
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			svc := newTestService(t, &fakeClient{tenancy: oci.TenancyInfo{Name: "t"}})
			cfg, err := svc.Import(context.Background(), trialImportInput())
			if err != nil {
				t.Fatalf("Import: %v", err)
			}
			updated, _, err := svc.Update(context.Background(), cfg.ID, tt.in)
			if err != nil {
				t.Fatalf("Update: %v", err)
			}
			if updated.LastVerifiedAt == nil || !updated.LastVerifiedAt.After(*cfg.LastVerifiedAt) {
				t.Error("Update should re-verify: LastVerifiedAt not advanced")
			}
			tt.check(t, svc, updated)
		})
	}
}

func TestUpdateKeyRequiresFingerprint(t *testing.T) {
	svc := newTestService(t, &fakeClient{tenancy: oci.TenancyInfo{Name: "t"}})
	cfg, err := svc.Import(context.Background(), trialImportInput())
	if err != nil {
		t.Fatalf("Import: %v", err)
	}
	if _, _, err := svc.Update(context.Background(), cfg.ID, UpdateInput{PrivateKey: "x"}); err == nil {
		t.Error("Update with key but no fingerprint: got nil error, want rejection")
	}
}

func TestListReturnsSummaries(t *testing.T) {
	svc := newTestService(t, &fakeClient{tenancy: oci.TenancyInfo{Name: "mytenancy"}})
	first, err := svc.Import(context.Background(), trialImportInput())
	if err != nil {
		t.Fatalf("Import first: %v", err)
	}
	second := trialImportInput()
	second.Alias = "生产号"
	second.Group = ""
	if _, err := svc.Import(context.Background(), second); err != nil {
		t.Fatalf("Import second: %v", err)
	}

	items, err := svc.List(context.Background())
	if err != nil {
		t.Fatalf("List: %v", err)
	}
	if len(items) != 2 {
		t.Fatalf("len(items) = %d, want 2", len(items))
	}
	got := items[0]
	want := ConfigSummary{
		ID: first.ID, Alias: "试用期", Group: "教育",
		TenancyOCID: first.TenancyOCID, TenancyName: "mytenancy", Region: first.Region,
		AccountType: first.AccountType, AliveStatus: model.AliveStatusAlive,
		LastVerifiedAt: got.LastVerifiedAt, CreatedAt: got.CreatedAt,
	}
	if got != want {
		t.Errorf("items[0] = %+v, want %+v", got, want)
	}
	if items[1].Group != "" || items[1].Alias != "生产号" {
		t.Errorf("items[1] alias/group = %q/%q, want 生产号/empty", items[1].Alias, items[1].Group)
	}
}

// TestVerifySuspendedOverridesStatus 云端暂停标记应覆盖测活结论为 suspended;
// 能力接口失败或未标记时不改变原结论。
func TestVerifySuspendedOverridesStatus(t *testing.T) {
	client := &fakeClient{tenancy: oci.TenancyInfo{Name: "t"}}
	svc := newTestService(t, client)
	cfg := importAliveConfig(t, svc)
	ctx := context.Background()

	// 标记暂停:即使 ValidateKey 成功也应判 suspended
	client.caps = oci.AccountCapabilities{Suspended: true, AccountStatus: "terminated"}
	got, _, err := svc.Verify(ctx, cfg.ID)
	if err != nil {
		t.Fatalf("Verify: %v", err)
	}
	if got.AliveStatus != model.AliveStatusSuspended {
		t.Fatalf("AliveStatus = %q, want suspended", got.AliveStatus)
	}
	if got.LastError == "" {
		t.Error("暂停时 LastError 应携带账户状态备注")
	}

	// 解除暂停:恢复 alive
	client.caps = oci.AccountCapabilities{}
	got, _, err = svc.Verify(ctx, cfg.ID)
	if err != nil || got.AliveStatus != model.AliveStatusAlive {
		t.Fatalf("解除暂停后 = %q, %v; want alive", got.AliveStatus, err)
	}

	// 能力接口失败:不影响测活结论
	client.capsErr = errors.New("boom")
	got, _, err = svc.Verify(ctx, cfg.ID)
	if err != nil || got.AliveStatus != model.AliveStatusAlive {
		t.Fatalf("能力接口失败后 = %q, %v; want alive", got.AliveStatus, err)
	}
}
