package metadata

import (
	"context"
	"database/sql"
	"errors"
	"testing"

	apperrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/kernel/pkg/ports"
	"github.com/nexuscrm/shared/pkg/contract"
	sharedErrors "github.com/nexuscrm/shared/pkg/errors"
	"github.com/nexuscrm/shared/pkg/models"
)

type activateMissingFlowRepo struct {
	ports.MetadataRepository
	activateCalled bool
}

func (r *activateMissingFlowRepo) GetFlowVersion(context.Context, string) (*models.FlowVersion, error) {
	return nil, nil
}

func (r *activateMissingFlowRepo) ActivateFlowVersion(context.Context, string) error {
	r.activateCalled = true
	return nil
}

func TestActivateFlowVersionMissingFlowReturnsNotFound(t *testing.T) {
	repo := &activateMissingFlowRepo{}
	svc := NewMetadataService(repo, nil, nil)

	err := svc.ActivateFlowVersion(context.Background(), "301-missing")
	if !apperrors.IsNotFound(err) {
		t.Fatalf("ActivateFlowVersion() error = %T %v, want NotFoundError", err, err)
	}
	if repo.activateCalled {
		t.Fatal("repo.ActivateFlowVersion was called for a missing Flow")
	}
}

type activateRaceMissingFlowRepo struct {
	ports.MetadataRepository
}

func (r *activateRaceMissingFlowRepo) GetFlowVersion(context.Context, string) (*models.FlowVersion, error) {
	return &models.FlowVersion{Id: "301-race-missing"}, nil
}

func (r *activateRaceMissingFlowRepo) ActivateFlowVersion(context.Context, string) error {
	return sql.ErrNoRows
}

func TestActivateFlowVersionRaceMissingFlowReturnsNotFound(t *testing.T) {
	repo := &activateRaceMissingFlowRepo{}
	svc := NewMetadataService(repo, nil, nil)

	err := svc.ActivateFlowVersion(context.Background(), "301-race-missing")
	if !apperrors.IsNotFound(err) {
		t.Fatalf("ActivateFlowVersion() error = %T %v, want NotFoundError", err, err)
	}
}

type flowAdmissionRepo struct {
	ports.MetadataRepository
	definition          *models.FlowDefinition
	patchCalled         bool
	markCalled          bool
	createVersionCalled bool
	setLatestCalled     bool
}

func (r *flowAdmissionRepo) GetFlowDefinition(context.Context, string) (*models.FlowDefinition, error) {
	return r.definition, nil
}

func (r *flowAdmissionRepo) PatchFlowDefinition(context.Context, string, *models.FlowDefinitionPatch) error {
	r.patchCalled = true
	return nil
}

func (r *flowAdmissionRepo) MarkFlowDefinitionCustomized(context.Context, string) error {
	r.markCalled = true
	return nil
}

func (r *flowAdmissionRepo) LockFlowDefinitionForUpdate(context.Context, string) error {
	return nil
}

func (r *flowAdmissionRepo) GetFlowVersionsForUpdate(context.Context, string) ([]*models.FlowVersion, error) {
	return nil, nil
}

func (r *flowAdmissionRepo) CreateFlowVersion(context.Context, *models.FlowVersion) error {
	r.createVersionCalled = true
	return nil
}

func (r *flowAdmissionRepo) SetLatestVersionId(context.Context, string, string) error {
	r.setLatestCalled = true
	return nil
}

func TestApplyFlowDefinitionPatchRejectsPackOwnedDefinitionBeforeWrite(t *testing.T) {
	repo := &flowAdmissionRepo{
		definition: &models.FlowDefinition{
			Id:            "300-pack",
			DeveloperName: "Pack_Flow",
			OwnerPack:     "salesforce-crm-standard",
			IsCustomized:  false,
		},
	}
	svc := NewMetadataService(repo, nil, nil)

	description := "admin overwrite"
	err := svc.ApplyFlowDefinitionPatch(context.Background(), "300-pack", contract.FlowAdminDefinitionPatchIntent{
		Action:      contract.FlowAdminDefinitionPatchActionUpdate,
		Description: &description,
	})
	if err == nil {
		t.Fatal("ApplyFlowDefinitionPatch() error = nil, want pack-owned FlowDefinition admission rejection")
	}
	assertInvalidOperation(t, err)
	if repo.patchCalled {
		t.Fatal("PatchFlowDefinition was called for pack-owned FlowDefinition")
	}
	if repo.markCalled {
		t.Fatal("MarkFlowDefinitionCustomized was called for pack-owned FlowDefinition")
	}
}

func TestCreateFlowVersionRejectsPackOwnedDefinitionBeforeWrite(t *testing.T) {
	repo := &flowAdmissionRepo{
		definition: &models.FlowDefinition{
			Id:            "300-pack",
			DeveloperName: "Pack_Flow",
			OwnerPack:     "salesforce-crm-standard",
			IsCustomized:  false,
		},
	}
	svc := NewMetadataService(repo, nil, nil)

	err := svc.CreateFlowVersion(context.Background(), &models.FlowVersion{
		Id:           "301-new",
		DefinitionId: "300-pack",
		Status:       models.FlowStatusDraft,
		ProcessType:  "AutoLaunchedFlow",
		Metadata:     models.FlowMetadata{},
	})
	if err == nil {
		t.Fatal("CreateFlowVersion() error = nil, want pack-owned FlowDefinition admission rejection")
	}
	if repo.createVersionCalled {
		t.Fatal("CreateFlowVersion repository write was called for pack-owned FlowDefinition")
	}
	if repo.setLatestCalled {
		t.Fatal("SetLatestVersionId was called for pack-owned FlowDefinition")
	}
	if repo.markCalled {
		t.Fatal("MarkFlowDefinitionCustomized was called for pack-owned FlowDefinition")
	}
}

func TestApplyFlowDefinitionPatchRejectsUnadmittedDefinitionBeforeWrite(t *testing.T) {
	repo := &flowAdmissionRepo{
		definition: &models.FlowDefinition{
			Id:            "300-unknown-owner",
			DeveloperName: "Unknown_Owner_Flow",
			OwnerPack:     "",
			IsCustomized:  false,
		},
	}
	svc := NewMetadataService(repo, nil, nil)

	description := "admin overwrite"
	err := svc.ApplyFlowDefinitionPatch(context.Background(), "300-unknown-owner", contract.FlowAdminDefinitionPatchIntent{
		Action:      contract.FlowAdminDefinitionPatchActionUpdate,
		Description: &description,
	})
	if err == nil {
		t.Fatal("ApplyFlowDefinitionPatch() error = nil, want unadmitted FlowDefinition ownership rejection")
	}
	assertInvalidOperation(t, err)
	if repo.patchCalled {
		t.Fatal("PatchFlowDefinition was called for unadmitted FlowDefinition")
	}
	if repo.markCalled {
		t.Fatal("MarkFlowDefinitionCustomized was called for unadmitted FlowDefinition")
	}
}

func TestApplyFlowDefinitionPatchIntentFailsClosed(t *testing.T) {
	description := "mixed"
	name := "Renamed_Flow"
	tests := []struct {
		name  string
		patch contract.FlowAdminDefinitionPatchIntent
		field string
	}{
		{
			name:  "zero action",
			patch: contract.FlowAdminDefinitionPatchIntent{},
			field: "Action",
		},
		{
			name: "update carries active version",
			patch: contract.FlowAdminDefinitionPatchIntent{
				Action:              contract.FlowAdminDefinitionPatchActionUpdate,
				Name:                &name,
				ActiveVersionNumber: 2,
			},
			field: "ActiveVersionNumber",
		},
		{
			name: "activate missing version number",
			patch: contract.FlowAdminDefinitionPatchIntent{
				Action: contract.FlowAdminDefinitionPatchActionActivateVersion,
			},
			field: "ActiveVersionNumber",
		},
		{
			name: "activate carries update field",
			patch: contract.FlowAdminDefinitionPatchIntent{
				Action:      contract.FlowAdminDefinitionPatchActionActivateVersion,
				Description: &description,
			},
			field: "FlowDefinition",
		},
		{
			name: "deactivate carries active version",
			patch: contract.FlowAdminDefinitionPatchIntent{
				Action:              contract.FlowAdminDefinitionPatchActionDeactivate,
				ActiveVersionNumber: 2,
			},
			field: "FlowDefinition",
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			repo := &flowAdmissionRepo{
				definition: &models.FlowDefinition{
					Id:           "300-flow",
					IsCustomized: true,
				},
			}
			svc := NewMetadataService(repo, nil, nil)

			err := svc.ApplyFlowDefinitionPatch(context.Background(), "300-flow", tc.patch)
			assertInvalidWireField(t, err, tc.field)
			if repo.patchCalled {
				t.Fatal("PatchFlowDefinition was called for invalid intent")
			}
			if repo.markCalled {
				t.Fatal("MarkFlowDefinitionCustomized was called for invalid intent")
			}
		})
	}
}

type flowVersionPatchValidationRepo struct {
	ports.MetadataRepository
	definition       *models.FlowDefinition
	version          *models.FlowVersion
	updateCalled     bool
	markCalled       bool
	activateCalled   bool
	deactivateCalled bool
}

func (r *flowVersionPatchValidationRepo) GetFlowVersion(context.Context, string) (*models.FlowVersion, error) {
	return r.version, nil
}

func (r *flowVersionPatchValidationRepo) GetFlowDefinition(context.Context, string) (*models.FlowDefinition, error) {
	return r.definition, nil
}

func (r *flowVersionPatchValidationRepo) UpdateFlowVersion(context.Context, string, *models.FlowVersion) error {
	r.updateCalled = true
	return nil
}

func (r *flowVersionPatchValidationRepo) ActivateFlowVersion(context.Context, string) error {
	r.activateCalled = true
	return nil
}

func (r *flowVersionPatchValidationRepo) DeactivateFlowVersion(context.Context, string) error {
	r.deactivateCalled = true
	return nil
}

func (r *flowVersionPatchValidationRepo) MarkFlowDefinitionCustomized(context.Context, string) error {
	r.markCalled = true
	return nil
}

func TestApplyFlowVersionPatchRejectsMalformedTriggerMetadataBeforeWrite(t *testing.T) {
	repo := &flowVersionPatchValidationRepo{
		definition: &models.FlowDefinition{
			Id:           "300-flow",
			ProcessType:  "AutoLaunchedFlow",
			IsCustomized: true,
		},
		version: &models.FlowVersion{
			Id:           "301-flow-v2",
			DefinitionId: "300-flow",
			Status:       models.FlowStatusDraft,
			ProcessType:  "AutoLaunchedFlow",
			MetadataRaw:  []byte(`{}`),
		},
	}
	svc := NewMetadataService(repo, nil, nil)

	err := svc.ApplyFlowVersionPatch(context.Background(), "301-flow-v2", contract.FlowAdminVersionPatchIntent{
		Action:           contract.FlowAdminVersionPatchActionUpdate,
		MetadataPatchRaw: []byte(`{"start":{"object":"Account"}}`),
	})
	if err == nil {
		t.Fatal("ApplyFlowVersionPatch() error = nil, want malformed trigger metadata rejection")
	}
	var wireErr *sharedErrors.DomainWireError
	if !errors.As(err, &wireErr) {
		t.Fatalf("ApplyFlowVersionPatch() error = %T %v, want DomainWireError", err, err)
	}
	if wireErr.Kind != sharedErrors.WireInvalidField {
		t.Fatalf("error kind = %q, want %q", wireErr.Kind, sharedErrors.WireInvalidField)
	}
	if len(wireErr.Fields) != 1 || wireErr.Fields[0] != "Metadata" {
		t.Fatalf("error fields = %v, want [Metadata]", wireErr.Fields)
	}
	if repo.updateCalled {
		t.Fatal("UpdateFlowVersion was called for malformed trigger metadata")
	}
	if repo.markCalled {
		t.Fatal("MarkFlowDefinitionCustomized was called for malformed trigger metadata")
	}
}

func TestApplyFlowVersionPatchIntentFailsClosed(t *testing.T) {
	tests := []struct {
		name  string
		patch contract.FlowAdminVersionPatchIntent
		field string
	}{
		{
			name:  "zero action",
			patch: contract.FlowAdminVersionPatchIntent{},
			field: "Action",
		},
		{
			name: "update carries no fields",
			patch: contract.FlowAdminVersionPatchIntent{
				Action: contract.FlowAdminVersionPatchActionUpdate,
			},
			field: "Flow",
		},
		{
			name: "activate carries description",
			patch: contract.FlowAdminVersionPatchIntent{
				Action:              contract.FlowAdminVersionPatchActionActivate,
				Description:         "mixed",
				DescriptionProvided: true,
			},
			field: "Flow",
		},
		{
			name: "deactivate carries metadata",
			patch: contract.FlowAdminVersionPatchIntent{
				Action:           contract.FlowAdminVersionPatchActionDeactivate,
				MetadataPatchRaw: []byte(`{"description":"mixed"}`),
			},
			field: "Flow",
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			repo := &flowVersionPatchValidationRepo{
				definition: &models.FlowDefinition{
					Id:           "300-flow",
					IsCustomized: true,
				},
				version: &models.FlowVersion{
					Id:           "301-flow-v2",
					DefinitionId: "300-flow",
					Status:       models.FlowStatusDraft,
					ProcessType:  "AutoLaunchedFlow",
					MetadataRaw:  []byte(`{}`),
				},
			}
			svc := NewMetadataService(repo, nil, nil)

			err := svc.ApplyFlowVersionPatch(context.Background(), "301-flow-v2", tc.patch)
			assertInvalidWireField(t, err, tc.field)
			if repo.updateCalled {
				t.Fatal("UpdateFlowVersion was called for invalid intent")
			}
			if repo.activateCalled {
				t.Fatal("ActivateFlowVersion was called for invalid intent")
			}
			if repo.deactivateCalled {
				t.Fatal("DeactivateFlowVersion was called for invalid intent")
			}
			if repo.markCalled {
				t.Fatal("MarkFlowDefinitionCustomized was called for invalid intent")
			}
		})
	}
}

func assertInvalidWireField(t *testing.T, err error, field string) {
	t.Helper()
	if err == nil {
		t.Fatalf("error = nil, want invalid field error for %s", field)
	}
	var wireErr *sharedErrors.DomainWireError
	if !errors.As(err, &wireErr) {
		t.Fatalf("error = %T %v, want DomainWireError", err, err)
	}
	if wireErr.Kind != sharedErrors.WireInvalidField {
		t.Fatalf("error kind = %q, want %q", wireErr.Kind, sharedErrors.WireInvalidField)
	}
	if len(wireErr.Fields) != 1 || wireErr.Fields[0] != field {
		t.Fatalf("error fields = %v, want [%s]", wireErr.Fields, field)
	}
}

func assertInvalidOperation(t *testing.T, err error) {
	t.Helper()
	if err == nil {
		t.Fatal("error = nil, want invalid operation")
	}
	var wireErr *sharedErrors.DomainWireError
	if !errors.As(err, &wireErr) {
		t.Fatalf("error = %T %v, want DomainWireError", err, err)
	}
	if wireErr.Kind != sharedErrors.WireInvalidOperation {
		t.Fatalf("error kind = %q, want %q", wireErr.Kind, sharedErrors.WireInvalidOperation)
	}
}
