package services

import (
	"context"
	"encoding/json"
	"strings"
	"testing"
	"time"

	"github.com/nexuscrm/shared/pkg/contract"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/nexuscrm/shared/pkg/queryspec"
)

func TestMetadataDeployContractStoresAndLoadsNeutralStatus(t *testing.T) {
	repo := &metadataDeploySystemRepo{configs: map[string]string{}}
	sm := &ServiceManager{System: &SystemManager{repo: repo}}

	jobID, err := sm.StartMetadataDeploy(context.Background())
	if err != nil {
		t.Fatalf("StartMetadataDeploy: %v", err)
	}
	if len(jobID) != 15 {
		t.Fatalf("jobID = %q, want 15-char unprefixed id", jobID)
	}
	var initial contract.MetadataDeployResult
	if err := json.Unmarshal([]byte(repo.configs[metadataDeployConfigPrefix+jobID]), &initial); err != nil {
		t.Fatalf("initial status JSON: %v", err)
	}
	if initial.Done || initial.Status != "InProgress" {
		t.Fatalf("initial = %+v, want InProgress", initial)
	}

	if err := sm.CompleteMetadataDeploy(context.Background(), jobID, contract.MetadataDeployResult{
		Done:    true,
		Status:  "Failed",
		Success: false,
		ComponentFailures: []contract.MetadataDeployComponentFailure{{
			Problem:  "Invalid XML format",
			FileName: "approvalProcesses/Account.Bad.approvalProcess",
		}},
	}); err != nil {
		t.Fatalf("CompleteMetadataDeploy: %v", err)
	}
	got, found, err := sm.GetMetadataDeployResult(context.Background(), jobID)
	if err != nil {
		t.Fatalf("GetMetadataDeployResult: %v", err)
	}
	if !found || got == nil || !got.Done || got.Status != "Failed" || len(got.ComponentFailures) != 1 {
		t.Fatalf("loaded = (%+v, %v), want failed status with component failure", got, found)
	}
}

func TestMetadataDeployContractReadsLegacySDKStoredStatus(t *testing.T) {
	repo := &metadataDeploySystemRepo{configs: map[string]string{
		metadataDeployConfigPrefix + "legacydeploy001": `{"done":true,"status":"Failed","details":{"componentFailures":[{"problem":"Invalid XML format","fileName":"approvalProcesses/Opportunity.Bad.approvalProcess","componentType":"ApprovalProcess"}]}}`,
	}}
	sm := &ServiceManager{System: &SystemManager{repo: repo}}

	got, found, err := sm.GetMetadataDeployResult(context.Background(), "legacydeploy001")
	if err != nil {
		t.Fatalf("GetMetadataDeployResult: %v", err)
	}
	if !found || got == nil {
		t.Fatalf("found/result = (%v,%+v), want legacy result", found, got)
	}
	if !got.Done || got.Status != "Failed" || len(got.ComponentFailures) != 1 {
		t.Fatalf("legacy result = %+v, want nested component failure folded into neutral result", got)
	}
	failure := got.ComponentFailures[0]
	if failure.Problem != "Invalid XML format" ||
		failure.FileName != "approvalProcesses/Opportunity.Bad.approvalProcess" ||
		failure.ComponentType != "ApprovalProcess" {
		t.Fatalf("legacy failure = %+v", failure)
	}
}

func TestMetadataDeployMatchFilterIsStableAndEscapesValues(t *testing.T) {
	filter, err := metadataDeployMatchFilter(map[string]string{
		"TableEnumOrId": "Opportunity",
		"DeveloperName": "Deal's Approval",
	})
	if err != nil {
		t.Fatalf("metadataDeployMatchFilter: %v", err)
	}
	if filter != `DeveloperName == 'Deal\'s Approval' && TableEnumOrId == 'Opportunity'` {
		t.Fatalf("filter = %q", filter)
	}
}

func TestMetadataDeployLifecycleCapabilityConfiguredRequiresExactCapability(t *testing.T) {
	schema := &models.ObjectMetadata{
		AccessPolicy: &models.ObjectAccessPolicy{
			Lifecycle: &models.RecordLifecycleAccessPolicy{
				Hooks: []models.RecordLifecycleHookPolicy{
					{Phase: string(contract.RecordLifecycleAfterCreateInTx), Capability: "crm.other"},
					{Phase: string(contract.RecordLifecycleAfterUpdateInTx), Capability: " crm.approval-process-child-sync "},
				},
			},
		},
	}
	if metadataDeployLifecycleCapabilityConfigured(schema, contract.RecordLifecycleAfterCreateInTx, "crm.approval-process-child-sync") {
		t.Fatal("unrelated create capability matched")
	}
	if !metadataDeployLifecycleCapabilityConfigured(schema, contract.RecordLifecycleAfterUpdateInTx, "crm.approval-process-child-sync") {
		t.Fatal("exact update capability did not match")
	}
}

func TestMetadataDeployDeleteRequiresExactlyOneUniqueMatch(t *testing.T) {
	tests := []struct {
		name        string
		matchFields map[string]string
		records     []contract.Record
		wantDeleted bool
		wantError   string
	}{
		{
			name: "one exact match",
			matchFields: map[string]string{
				"DeveloperName": "Legacy_Approval",
				"OwnerPack":     "",
			},
			records:     []contract.Record{{"Id": "04a-legacy"}},
			wantDeleted: true,
		},
		{
			name: "missing match",
			matchFields: map[string]string{
				"DeveloperName": "Missing_Approval",
			},
			wantError: "requires exactly one matching ProcessDefinition record",
		},
		{
			name: "duplicate match",
			matchFields: map[string]string{
				"DeveloperName": "Duplicate_Approval",
			},
			records:   []contract.Record{{"Id": "04a-one"}, {"Id": "04a-two"}},
			wantError: "requires exactly one matching ProcessDefinition record",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			query := &metadataDeployDeleteQuery{result: &contract.QueryResult{Records: tt.records}}
			deleter := &metadataDeployDeleteDeleter{}
			coordinator := &metadataDeployDeleteCoordinator{}
			sm := &ServiceManager{
				NeutralQuery:                     query,
				NeutralRecords:                   &fakeContractRecords{},
				NeutralRecordMutationCoordinator: coordinator,
				metadataDeployRecordDeleter:      deleter,
			}
			if _, ok := sm.RecordsContract().(contract.MetadataDeployRecordDeleter); ok {
				t.Fatal("ordinary RecordsContract exposes metadata-deploy delete capability")
			}

			result, err := sm.ApplyMetadataDeployRecord(context.Background(), contract.MetadataDeployRecordRequest{
				ObjectAPIName: "ProcessDefinition",
				MatchFields:   tt.matchFields,
				Operation:     contract.MetadataDeployRecordOperationDelete,
			}, &models.UserSession{ID: "system"})
			if tt.wantError != "" {
				if err == nil || !strings.Contains(err.Error(), tt.wantError) {
					t.Fatalf("ApplyMetadataDeployRecord error = %v, want %q", err, tt.wantError)
				}
				if result != nil && result.Deleted {
					t.Fatalf("result = %+v, want no deletion", result)
				}
				if deleter.deletedID != "" {
					t.Fatalf("deleted id = %q, want no mutation", deleter.deletedID)
				}
				return
			}

			if err != nil {
				t.Fatalf("ApplyMetadataDeployRecord: %v", err)
			}
			if result == nil || result.Deleted != tt.wantDeleted {
				t.Fatalf("result = %+v, want Deleted=%v", result, tt.wantDeleted)
			}
			if deleter.deletedObject != "ProcessDefinition" || deleter.deletedID != "04a-legacy" {
				t.Fatalf("delete = %s/%s, want ProcessDefinition/04a-legacy", deleter.deletedObject, deleter.deletedID)
			}
			if owner, ok := deleter.requiredMatch["OwnerPack"]; !ok || owner != "" {
				t.Fatalf("locked delete match = %#v, want explicit ownerless OwnerPack", deleter.requiredMatch)
			}
			if !coordinator.ran {
				t.Fatal("delete did not run in pessimistic transaction")
			}
			if query.request.Limit != 2 {
				t.Fatalf("delete query limit = %d, want 2 to detect duplicate matches", query.request.Limit)
			}
			if got := query.request.Filter.Expression; got != "DeveloperName == 'Legacy_Approval' && OwnerPack == ''" {
				t.Fatalf("delete filter = %q", got)
			}
		})
	}
}

func TestMetadataDeployRecordRejectsUnknownOperation(t *testing.T) {
	sm := &ServiceManager{NeutralQuery: &metadataDeployDeleteQuery{}}

	_, err := sm.ApplyMetadataDeployRecord(context.Background(), contract.MetadataDeployRecordRequest{
		ObjectAPIName: "ProcessDefinition",
		MatchFields:   map[string]string{"DeveloperName": "Legacy_Approval"},
		Operation:     contract.MetadataDeployRecordOperation("replace"),
	}, &models.UserSession{ID: "system"})
	if err == nil || !strings.Contains(err.Error(), "unsupported metadata deploy record operation") {
		t.Fatalf("ApplyMetadataDeployRecord error = %v, want unsupported-operation rejection", err)
	}
}

type metadataDeployDeleteQueryFallback interface {
	contract.Query
}

type metadataDeployDeleteQuery struct {
	metadataDeployDeleteQueryFallback
	result  *contract.QueryResult
	request queryspec.QuerySpec
}

func (q *metadataDeployDeleteQuery) Query(_ context.Context, req queryspec.QuerySpec, _ *models.UserSession) (*contract.QueryResult, error) {
	q.request = req
	return q.result, nil
}

type metadataDeployDeleteDeleter struct {
	deletedObject string
	deletedID     string
	requiredMatch map[string]string
}

func (r *metadataDeployDeleteDeleter) DeleteMetadataDeployRecord(_ context.Context, objectName, id string, requiredMatch map[string]string, _ *models.UserSession) error {
	r.deletedObject = objectName
	r.deletedID = id
	r.requiredMatch = requiredMatch
	return nil
}

type metadataDeployDeleteCoordinator struct {
	contract.RecordMutationCoordinator
	ran bool
}

func (c *metadataDeployDeleteCoordinator) RunInPessimisticTransaction(ctx context.Context, fn func(context.Context) error) error {
	c.ran = true
	return fn(ctx)
}

func (*metadataDeployDeleteCoordinator) RequiresSourceAwareTransaction(string) bool {
	return false
}

func TestMetadataDeployCleanupContextIsDetachedAndBounded(t *testing.T) {
	type unsafeKey struct{}

	ctx, cancel := metadataDeployCleanupContext()
	defer cancel()

	if err := ctx.Err(); err != nil {
		t.Fatalf("cleanup context starts canceled: %v", err)
	}
	deadline, ok := ctx.Deadline()
	if !ok {
		t.Fatal("cleanup context should be bounded by a deadline")
	}
	remaining := time.Until(deadline)
	if remaining <= 0 || remaining > metadataDeployCleanupTimeout {
		t.Fatalf("cleanup deadline remaining = %s, want within %s", remaining, metadataDeployCleanupTimeout)
	}
	if got := ctx.Value(unsafeKey{}); got != nil {
		t.Fatalf("unsafe request-scoped value leaked into cleanup context: %#v", got)
	}
}

type metadataDeploySystemRepo struct {
	configs map[string]string
}

func (r *metadataDeploySystemRepo) GetLogs(context.Context, int) ([]*models.SystemLog, error) {
	return nil, nil
}

func (r *metadataDeploySystemRepo) GetRecentItems(context.Context, string, int) ([]*models.SystemRecent, error) {
	return nil, nil
}

func (r *metadataDeploySystemRepo) GetConfig(_ context.Context, key string) (*string, error) {
	value, ok := r.configs[key]
	if !ok {
		return nil, nil
	}
	return &value, nil
}

func (r *metadataDeploySystemRepo) GetAllConfigs(context.Context) ([]*models.SystemConfig, error) {
	return nil, nil
}

func (r *metadataDeploySystemRepo) GetConfigID(context.Context, string) (string, error) {
	return "", nil
}

func (r *metadataDeploySystemRepo) UpsertConfig(_ context.Context, config *models.SystemConfig) error {
	if r.configs == nil {
		r.configs = map[string]string{}
	}
	r.configs[config.KeyName] = config.Value
	return nil
}

func (r *metadataDeploySystemRepo) DeleteConfig(_ context.Context, key string) error {
	delete(r.configs, key)
	return nil
}

func (r *metadataDeploySystemRepo) GetProfileIDByName(context.Context, string) (string, error) {
	return "", nil
}

func (r *metadataDeploySystemRepo) GetUserIDByEmail(context.Context, string) (string, error) {
	return "", nil
}

func (r *metadataDeploySystemRepo) BatchUpsertProfiles(context.Context, []models.Profile) error {
	return nil
}

func (r *metadataDeploySystemRepo) BatchUpsertUsers(context.Context, []models.User) error {
	return nil
}

func (r *metadataDeploySystemRepo) ForceUpdateUserPassword(context.Context, string, string) error {
	return nil
}

func (r *metadataDeploySystemRepo) GetConnectedAppByClientID(context.Context, string) (*models.SystemConnectedApp, error) {
	return nil, nil
}

func (r *metadataDeploySystemRepo) IsUserPermittedForApp(context.Context, string, string) (bool, error) {
	return false, nil
}

func (r *metadataDeploySystemRepo) WithConnectedAppCredentialIssuance(
	ctx context.Context,
	_ string,
	issue func(context.Context) error,
) error {
	return issue(ctx)
}
