package provision

import (
	"context"
	"testing"

	"sub2api-cn-relay-manager/internal/host/sub2api"
	"sub2api-cn-relay-manager/internal/store/sqlite"
)

func TestBatchDetailServiceGetReturnsPersistedArtifacts(t *testing.T) {
	store := openProvisionTestStore(t)
	defer closeProvisionTestStore(t, store)

	host := &fakeHostAdapter{
		batchAccounts: []sub2api.AccountRef{{ID: "account_1", Name: "deepseek-01"}, {ID: "account_2", Name: "deepseek-02"}},
		testResults: map[string]sub2api.ProbeResult{
			"account_1": {OK: true, Status: "passed"},
			"account_2": {OK: true, Status: "passed"},
		},
		models: map[string][]sub2api.AccountModel{
			"account_1": {{ID: "deepseek-chat"}},
			"account_2": {{ID: "deepseek-chat"}},
		},
		gatewayResult: sub2api.GatewayAccessResult{OK: true, StatusCode: 200, HasExpectedModel: true, Models: []string{"deepseek-chat"}},
	}

	batchID := seedRuntimeImportForReconcile(t, store, host)
	providerRow, err := store.Providers().ListByProviderID(context.Background(), sampleProviderManifest().ProviderID)
	if err != nil {
		t.Fatalf("Providers().ListByProviderID() error = %v", err)
	}
	if len(providerRow) != 1 {
		t.Fatalf("providers = %d, want 1", len(providerRow))
	}
	batchRow, err := store.ImportBatches().GetByID(context.Background(), batchID)
	if err != nil {
		t.Fatalf("ImportBatches().GetByID() error = %v", err)
	}
	if _, err := store.ReconcileRuns().Create(context.Background(), sqlite.ReconcileRun{BatchID: batchID, HostID: batchRow.HostID, ProviderID: providerRow[0].ID, Status: "active", SummaryJSON: `{"missing_count":0}`}); err != nil {
		t.Fatalf("ReconcileRuns().Create() error = %v", err)
	}

	result, err := NewBatchDetailService(store).Get(context.Background(), batchID)
	if err != nil {
		t.Fatalf("Get() error = %v", err)
	}
	if result.Batch.ID != batchID {
		t.Fatalf("Batch.ID = %d, want %d", result.Batch.ID, batchID)
	}
	if len(result.Items) != 2 {
		t.Fatalf("len(Items) = %d, want 2", len(result.Items))
	}
	if len(result.ManagedResources) != 4 {
		t.Fatalf("len(ManagedResources) = %d, want 4", len(result.ManagedResources))
	}
	if len(result.AccessClosures) != 1 {
		t.Fatalf("len(AccessClosures) = %d, want 1", len(result.AccessClosures))
	}
	if len(result.ReconcileRuns) != 1 {
		t.Fatalf("len(ReconcileRuns) = %d, want 1", len(result.ReconcileRuns))
	}
}

func TestBatchDetailServiceScopesReconcileRunsByBatchID(t *testing.T) {
	store := openProvisionTestStore(t)
	defer closeProvisionTestStore(t, store)

	host := &fakeHostAdapter{
		batchAccounts: []sub2api.AccountRef{{ID: "account_1", Name: "deepseek-01"}, {ID: "account_2", Name: "deepseek-02"}},
		testResults: map[string]sub2api.ProbeResult{
			"account_1": {OK: true, Status: "passed"},
			"account_2": {OK: true, Status: "passed"},
		},
		models: map[string][]sub2api.AccountModel{
			"account_1": {{ID: "deepseek-chat"}},
			"account_2": {{ID: "deepseek-chat"}},
		},
		gatewayResult: sub2api.GatewayAccessResult{OK: true, StatusCode: 200, HasExpectedModel: true, Models: []string{"deepseek-chat"}},
	}

	ctx := context.Background()
	batchID := seedRuntimeImportForReconcile(t, store, host)
	batchRow, err := store.ImportBatches().GetByID(ctx, batchID)
	if err != nil {
		t.Fatalf("ImportBatches().GetByID() error = %v", err)
	}
	providerRow, err := store.Providers().ListByProviderID(ctx, sampleProviderManifest().ProviderID)
	if err != nil {
		t.Fatalf("Providers().ListByProviderID() error = %v", err)
	}
	if len(providerRow) != 1 {
		t.Fatalf("providers = %d, want 1", len(providerRow))
	}
	secondBatchID, err := store.ImportBatches().Create(ctx, sqlite.ImportBatch{HostID: batchRow.HostID, PackID: batchRow.PackID, ProviderID: providerRow[0].ID, Mode: ImportModePartial, BatchStatus: BatchStatusSucceeded, AccessStatus: AccessStatusSelfServiceReady})
	if err != nil {
		t.Fatalf("ImportBatches().Create(second) error = %v", err)
	}
	if _, err := store.ReconcileRuns().Create(ctx, sqlite.ReconcileRun{BatchID: batchID, HostID: batchRow.HostID, ProviderID: providerRow[0].ID, Status: "drifted", SummaryJSON: `{"batch":"first"}`}); err != nil {
		t.Fatalf("ReconcileRuns().Create(first batch) error = %v", err)
	}
	if _, err := store.ReconcileRuns().Create(ctx, sqlite.ReconcileRun{BatchID: secondBatchID, HostID: batchRow.HostID, ProviderID: providerRow[0].ID, Status: "active", SummaryJSON: `{"batch":"second"}`}); err != nil {
		t.Fatalf("ReconcileRuns().Create(second batch) error = %v", err)
	}

	result, err := NewBatchDetailService(store).Get(ctx, secondBatchID)
	if err != nil {
		t.Fatalf("Get() error = %v", err)
	}
	if len(result.ReconcileRuns) != 1 {
		t.Fatalf("len(ReconcileRuns) = %d, want 1 for this batch only", len(result.ReconcileRuns))
	}
	if result.ReconcileRuns[0].Status != "active" {
		t.Fatalf("ReconcileRuns[0].Status = %q, want active", result.ReconcileRuns[0].Status)
	}
}

func TestBatchDetailServiceGetValidatesStore(t *testing.T) {
	_, err := (*BatchDetailService)(nil).Get(context.Background(), 1)
	if err == nil || err.Error() != "store is required" {
		t.Fatalf("nil service Get() error = %v, want store is required", err)
	}
}

func TestAccountIDFromProbeSummary(t *testing.T) {
	accountID, err := accountIDFromProbeSummary(`{"account_id":" account_1 "}`)
	if err != nil {
		t.Fatalf("accountIDFromProbeSummary() error = %v", err)
	}
	if accountID != "account_1" {
		t.Fatalf("accountID = %q, want account_1", accountID)
	}
	if _, err := accountIDFromProbeSummary(`{`); err == nil {
		t.Fatal("accountIDFromProbeSummary() error = nil, want JSON decode error")
	}
	blank, err := accountIDFromProbeSummary("")
	if err != nil {
		t.Fatalf("accountIDFromProbeSummary(blank) error = %v", err)
	}
	if blank != "" {
		t.Fatalf("blank accountID = %q, want empty", blank)
	}
}

func TestDiffManagedResourcesCountsMissingAndExtra(t *testing.T) {
	missing, extra := diffManagedResources(
		[]sqlite.ManagedResource{
			{ResourceType: "group", HostResourceID: "group_1"},
			{ResourceType: "account", HostResourceID: "account_1"},
		},
		sub2api.ManagedResourceSnapshot{
			Groups:   []sub2api.NamedResource{{ID: "group_1"}},
			Accounts: []sub2api.NamedResource{{ID: "account_2"}},
		},
	)
	if missing != 1 || extra != 1 {
		t.Fatalf("diffManagedResources() = (%d, %d), want (1, 1)", missing, extra)
	}
}

func TestDeriveProviderStatus(t *testing.T) {
	tests := []struct {
		name            string
		batchStatus     string
		accessStatus    string
		reconcileStatus string
		want            string
	}{
		{name: "recovered success beats stale reconcile", batchStatus: BatchStatusSucceeded, accessStatus: AccessStatusSelfServiceReady, reconcileStatus: "degraded", want: ProviderStatusActive},
		{name: "partial ready with active reconcile becomes active", batchStatus: BatchStatusPartial, accessStatus: AccessStatusSubscriptionReady, reconcileStatus: ProviderStatusActive, want: ProviderStatusActive},
		{name: "succeeded batch", batchStatus: BatchStatusSucceeded, reconcileStatus: "not_run", want: ProviderStatusActive},
		{name: "rolled back beats active reconcile", batchStatus: BatchStatusRolledBack, reconcileStatus: ProviderStatusActive, want: ProviderStatusFailed},
		{name: "failed beats active reconcile", batchStatus: BatchStatusFailed, reconcileStatus: ProviderStatusActive, want: ProviderStatusFailed},
		{name: "failed batch", batchStatus: BatchStatusFailed, want: ProviderStatusFailed},
		{name: "running batch", batchStatus: "running", want: "running"},
		{name: "unknown fallback", batchStatus: " pending ", want: "pending"},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			if got := deriveProviderStatus(tc.batchStatus, tc.accessStatus, tc.reconcileStatus); got != tc.want {
				t.Fatalf("deriveProviderStatus(%q, %q, %q) = %q, want %q", tc.batchStatus, tc.accessStatus, tc.reconcileStatus, got, tc.want)
			}
		})
	}
}

func TestProviderStatusServiceAggregatesLatestAccessModesAcrossBatches(t *testing.T) {
	store := openProvisionTestStore(t)
	defer closeProvisionTestStore(t, store)

	ctx := context.Background()
	hostID := seedProvisionHost(t, store, "host-1", "https://sub2api.example.com")
	packID, err := store.Packs().Create(ctx, sqlite.Pack{PackID: "openai-cn-pack", Version: "1.0.0", TargetHost: "sub2api", Checksum: "checksum-1"})
	if err != nil {
		t.Fatalf("Packs().Create() error = %v", err)
	}
	providerID, err := store.Providers().Create(ctx, sqlite.Provider{PackID: packID, ProviderID: "deepseek", DisplayName: "DeepSeek", BaseURL: "https://api.deepseek.com", Platform: "openai"})
	if err != nil {
		t.Fatalf("Providers().Create() error = %v", err)
	}
	batchSubscription, err := store.ImportBatches().Create(ctx, sqlite.ImportBatch{HostID: hostID, PackID: packID, ProviderID: providerID, Mode: ImportModePartial, BatchStatus: BatchStatusSucceeded, AccessStatus: AccessStatusSubscriptionReady})
	if err != nil {
		t.Fatalf("ImportBatches().Create(subscription) error = %v", err)
	}
	if _, err := store.AccessClosures().Create(ctx, sqlite.AccessClosureRecord{BatchID: batchSubscription, ClosureType: AccessModeSubscription, Status: AccessStatusSubscriptionReady, DetailsJSON: "{}"}); err != nil {
		t.Fatalf("AccessClosures().Create(subscription) error = %v", err)
	}
	batchSelfService, err := store.ImportBatches().Create(ctx, sqlite.ImportBatch{HostID: hostID, PackID: packID, ProviderID: providerID, Mode: ImportModePartial, BatchStatus: BatchStatusSucceeded, AccessStatus: AccessStatusSelfServiceReady})
	if err != nil {
		t.Fatalf("ImportBatches().Create(self_service) error = %v", err)
	}
	if _, err := store.AccessClosures().Create(ctx, sqlite.AccessClosureRecord{BatchID: batchSelfService, ClosureType: AccessModeSelfService, Status: AccessStatusSelfServiceReady, DetailsJSON: "{}"}); err != nil {
		t.Fatalf("AccessClosures().Create(self_service) error = %v", err)
	}
	if _, err := store.ReconcileRuns().Create(ctx, sqlite.ReconcileRun{BatchID: batchSelfService, HostID: hostID, ProviderID: providerID, Status: "drifted", SummaryJSON: `{"missing_count":1}`}); err != nil {
		t.Fatalf("ReconcileRuns().Create() error = %v", err)
	}

	snapshot, err := NewProviderStatusService(store).GetStatus(ctx, ProviderQuery{ProviderID: "deepseek", PackID: "openai-cn-pack", HostID: "host-1"})
	if err != nil {
		t.Fatalf("GetStatus() error = %v", err)
	}
	if snapshot.LatestAccessStatus != AccessStatusFullyReady {
		t.Fatalf("LatestAccessStatus = %q, want %q", snapshot.LatestAccessStatus, AccessStatusFullyReady)
	}
	if snapshot.ProviderStatus != ProviderStatusActive {
		t.Fatalf("ProviderStatus = %q, want %q", snapshot.ProviderStatus, ProviderStatusActive)
	}
	if snapshot.LatestReconcileStatus != "drifted" {
		t.Fatalf("LatestReconcileStatus = %q, want drifted", snapshot.LatestReconcileStatus)
	}
}

func TestBuildPackAndProviderRecord(t *testing.T) {
	packRow, err := buildPackRecord(sampleLoadedPack())
	if err != nil {
		t.Fatalf("buildPackRecord() error = %v", err)
	}
	if packRow.PackID != "openai-cn-pack" || packRow.TargetHost != "sub2api" {
		t.Fatalf("packRow = %#v, want populated pack metadata", packRow)
	}

	providerRow, err := buildProviderRecord(7, sampleProviderManifest())
	if err != nil {
		t.Fatalf("buildProviderRecord() error = %v", err)
	}
	if providerRow.PackID != 7 || providerRow.ProviderID != sampleProviderManifest().ProviderID {
		t.Fatalf("providerRow = %#v, want persisted provider metadata", providerRow)
	}
	if providerRow.DefaultModelsJSON == "" || providerRow.ManifestJSON == "" {
		t.Fatalf("providerRow JSON fields = %#v, want serialized JSON", providerRow)
	}
}

func TestFirstNonEmptyAndFingerprintKey(t *testing.T) {
	if got := firstNonEmpty(" ", "value", "other"); got != "value" {
		t.Fatalf("firstNonEmpty() = %q, want value", got)
	}
	if got := fingerprintKey([]string{" key-1 "}, 0); got == "key-1" || got == "sha256:" || len(got) < 20 {
		t.Fatalf("fingerprintKey() = %q, want sha256 fingerprint", got)
	}
	if got := fingerprintKey(nil, 3); got != "key-4" {
		t.Fatalf("fingerprintKey(nil, 3) = %q, want key-4", got)
	}
}

func TestProviderStatusServiceGetResourcesRequiresProviderID(t *testing.T) {
	store := openProvisionTestStore(t)
	defer closeProvisionTestStore(t, store)

	_, err := NewProviderStatusService(store).GetResources(context.Background(), ProviderQuery{})
	if err == nil || err.Error() != "provider_id is required" {
		t.Fatalf("GetResources() error = %v, want provider_id is required", err)
	}
}

func TestResourceSlugFallsBackToProvider(t *testing.T) {
	if got := resourceSlug("  !!!  "); got != "provider" {
		t.Fatalf("resourceSlug() = %q, want provider", got)
	}
	provider := sampleProviderManifest()
	provider.ProviderID = " DeepSeek CN / Prod "
	provider.GroupTemplate.Name = ""
	provider.ChannelTemplate.Name = ""
	provider.PlanTemplate.Name = ""
	if got := SuggestAccountNamePrefix(provider); got != "deepseek-cn-prod-" {
		t.Fatalf("SuggestAccountNamePrefix() = %q, want deepseek-cn-prod-", got)
	}
	resourceNames := SuggestResourceNames(provider)
	if resourceNames.Group != "crm-deepseek-cn-prod-group" {
		t.Fatalf("SuggestResourceNames() = %#v, want slugged resource names", resourceNames)
	}
}
