package metadata

import (
	"context"
	"errors"
	"testing"

	"github.com/nexuscrm/kernel/pkg/ports"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/contract"
	"github.com/nexuscrm/shared/pkg/models"
)

type metadataUpdateAppRepo struct {
	ports.MetadataRepository
	existing *models.AppConfig
	saved    *models.AppConfig
	created  *models.AppConfig
	getErr   error
}

func (r *metadataUpdateAppRepo) GetApp(ctx context.Context, id string) (*models.AppConfig, error) {
	if r.getErr != nil {
		return nil, r.getErr
	}
	if r.existing == nil {
		return nil, nil
	}
	cloned := *r.existing
	if r.existing.NavigationItems != nil {
		cloned.NavigationItems = append([]models.NavigationItem(nil), r.existing.NavigationItems...)
	}
	if r.existing.UtilityItems != nil {
		cloned.UtilityItems = append([]models.UtilityItem(nil), r.existing.UtilityItems...)
	}
	if r.existing.AssignedProfiles != nil {
		cloned.AssignedProfiles = append([]string(nil), r.existing.AssignedProfiles...)
	}
	return &cloned, nil
}

func (r *metadataUpdateAppRepo) UpdateApp(ctx context.Context, id string, app *models.AppConfig) error {
	cloned := *app
	if app.NavigationItems != nil {
		cloned.NavigationItems = append([]models.NavigationItem(nil), app.NavigationItems...)
	}
	if app.UtilityItems != nil {
		cloned.UtilityItems = append([]models.UtilityItem(nil), app.UtilityItems...)
	}
	if app.AssignedProfiles != nil {
		cloned.AssignedProfiles = append([]string(nil), app.AssignedProfiles...)
	}
	r.saved = &cloned
	return nil
}

func (r *metadataUpdateAppRepo) CreateApp(ctx context.Context, app *models.AppConfig) error {
	cloned := *app
	if app.NavigationItems != nil {
		cloned.NavigationItems = append([]models.NavigationItem(nil), app.NavigationItems...)
	}
	if app.UtilityItems != nil {
		cloned.UtilityItems = append([]models.UtilityItem(nil), app.UtilityItems...)
	}
	if app.AssignedProfiles != nil {
		cloned.AssignedProfiles = append([]string(nil), app.AssignedProfiles...)
	}
	r.created = &cloned
	return nil
}

func (r *metadataUpdateAppRepo) CreateAppMenuItem(ctx context.Context, app *models.AppConfig) error {
	return nil
}

func TestCreateAppRequiresFrontendAppID(t *testing.T) {
	repo := &metadataUpdateAppRepo{}
	ms := NewMetadataService(repo, nil, nil)

	err := ms.CreateApp(context.Background(), &models.AppConfig{
		ID:    "CustomConsole",
		Label: "Custom Console",
	})
	if err == nil {
		t.Fatal("CreateApp(ownerless) error = nil, want validation error")
	}
	if repo.created != nil {
		t.Fatalf("created app = %+v, want no write", repo.created)
	}
}

func TestCreateAppTrimsAndPersistsFrontendAppID(t *testing.T) {
	repo := &metadataUpdateAppRepo{}
	ms := NewMetadataService(repo, nil, nil)

	err := ms.CreateApp(context.Background(), &models.AppConfig{
		ID:            "CustomConsole",
		Label:         "Custom Console",
		FrontendAppID: " crm ",
	})
	if err != nil {
		t.Fatalf("CreateApp(with owner) = %v, want nil", err)
	}
	if repo.created == nil {
		t.Fatal("created app is nil, want write")
	}
	if repo.created.FrontendAppID != "crm" {
		t.Fatalf("created FrontendAppID = %q, want trimmed owner", repo.created.FrontendAppID)
	}
}

func TestUpdateAppWithPatchPresencePreservesOmittedIsDefault(t *testing.T) {
	repo := &metadataUpdateAppRepo{
		existing: &models.AppConfig{
			ID:        "Sales",
			Label:     "Sales",
			IsDefault: true,
		},
	}
	ms := NewMetadataService(repo, nil, nil)

	err := ms.UpdateAppWithPatchPresence(context.Background(), "Sales", &models.AppConfig{
		Label: "Sales Workspace",
	}, false)
	if err != nil {
		t.Fatalf("UpdateAppWithPatchPresence(omitted isDefault) = %v, want nil", err)
	}
	if repo.saved == nil {
		t.Fatal("saved app is nil, want updated app")
	}
	if !repo.saved.IsDefault {
		t.Fatalf("saved IsDefault = %v, want true preserved from existing app", repo.saved.IsDefault)
	}
	if repo.saved.Label != "Sales Workspace" {
		t.Fatalf("saved Label = %q, want updated label", repo.saved.Label)
	}
}

func TestUpdateAppWithPatchPresenceAppliesAuthoritativeIsDefault(t *testing.T) {
	repo := &metadataUpdateAppRepo{
		existing: &models.AppConfig{
			ID:        "Sales",
			Label:     "Sales",
			IsDefault: true,
		},
	}
	ms := NewMetadataService(repo, nil, nil)

	err := ms.UpdateAppWithPatchPresence(context.Background(), "Sales", &models.AppConfig{
		IsDefault: false,
	}, true)
	if err != nil {
		t.Fatalf("UpdateAppWithPatchPresence(authoritative isDefault) = %v, want nil", err)
	}
	if repo.saved == nil {
		t.Fatal("saved app is nil, want updated app")
	}
	if repo.saved.IsDefault {
		t.Fatalf("saved IsDefault = %v, want false after authoritative update", repo.saved.IsDefault)
	}
}

func TestUpdateAppWithPatchPresencePreservesOmittedFrontendAppID(t *testing.T) {
	repo := &metadataUpdateAppRepo{
		existing: &models.AppConfig{
			ID:            "Sales",
			Label:         "Sales",
			FrontendAppID: "crm",
		},
	}
	ms := NewMetadataService(repo, nil, nil)

	err := ms.UpdateAppWithPatchPresence(context.Background(), "Sales", &models.AppConfig{
		NavigationItems: []models.NavigationItem{{ID: "home", Type: constants.NavigationTypePage, Label: "Home"}},
	}, false)
	if err != nil {
		t.Fatalf("UpdateAppWithPatchPresence(omitted frontendAppId) = %v, want nil", err)
	}
	if repo.saved == nil {
		t.Fatal("saved app is nil, want updated app")
	}
	if repo.saved.FrontendAppID != "crm" {
		t.Fatalf("saved FrontendAppID = %q, want existing owner preserved", repo.saved.FrontendAppID)
	}
}

func TestUpdateAppShellDoesNotWriteNavigationSnapshot(t *testing.T) {
	repo := &metadataUpdateAppRepo{existing: &models.AppConfig{
		ID: "app", Label: "Before", FrontendAppID: "owner",
		NavigationItems: []models.NavigationItem{{ID: "stale", Type: constants.NavigationTypePage, Label: "Old"}},
	}}
	service := NewMetadataService(repo, nil, nil)
	if err := service.UpdateAppWithPatchPresence(context.Background(), "app", &models.AppConfig{Label: "After"}, false); err != nil {
		t.Fatal(err)
	}
	if repo.saved == nil || repo.saved.Label != "After" || repo.saved.NavigationItems != nil {
		t.Fatalf("shell patch wrote stale navigation: %+v", repo.saved)
	}
}

func TestUpdateAppWithPatchPresenceAppliesFrontendAppID(t *testing.T) {
	repo := &metadataUpdateAppRepo{
		existing: &models.AppConfig{
			ID:            "Sales",
			Label:         "Sales",
			FrontendAppID: "legacy-crm",
		},
	}
	ms := NewMetadataService(repo, nil, nil)

	err := ms.UpdateAppWithPatchPresence(context.Background(), "Sales", &models.AppConfig{
		FrontendAppID: "crm",
	}, false)
	if err != nil {
		t.Fatalf("UpdateAppWithPatchPresence(frontendAppId) = %v, want nil", err)
	}
	if repo.saved == nil {
		t.Fatal("saved app is nil, want updated app")
	}
	if repo.saved.FrontendAppID != "crm" {
		t.Fatalf("saved FrontendAppID = %q, want updated owner", repo.saved.FrontendAppID)
	}
}

func TestUpdateAppWithPatchPresenceAndNavigationIdentitiesPreservesExistingRichNavigationItems(t *testing.T) {
	repo := &metadataUpdateAppRepo{
		existing: &models.AppConfig{
			ID: "Sales",
			NavigationItems: []models.NavigationItem{
				{ID: "account", Type: constants.NavigationTypeObject, ObjectAPIName: "Account", Label: "Accounts", Icon: "Building2"},
				{ID: "home", Type: constants.NavigationTypePage, ObjectAPIName: "Home", Label: "Home", Icon: "Home", PageURL: "/"},
				{ID: "web-link-1", Type: constants.NavigationTypeWeb, Label: "Docs", Icon: "Globe", PageURL: "https://example.test/docs"},
			},
		},
	}
	ms := NewMetadataService(repo, nil, nil)

	err := ms.UpdateAppWithPatchPresenceAndNavigationIdentities(context.Background(), "Sales", &models.AppConfig{
		NavigationItems: []models.NavigationItem{
			{ID: "web-link-1", Type: constants.NavigationTypePage, Label: "web-link-1"},
			{ID: "nav-1-account", Type: constants.NavigationTypeObject, ObjectAPIName: "Account", Label: "Accounts"},
			{ID: "nav-2-home", Type: constants.NavigationTypePage, ObjectAPIName: "Home", Label: "Home"},
		},
	}, false, []contract.ApplicationNavigationItemIdentity{
		{Kind: contract.ApplicationNavigationItemIdentityID, Key: "web-link-1"},
		{Kind: contract.ApplicationNavigationItemIdentityObject, Key: "Account"},
		{Kind: contract.ApplicationNavigationItemIdentityPage, Key: "Home"},
	})
	if err != nil {
		t.Fatalf("UpdateAppWithPatchPresenceAndNavigationIdentities() error = %v, want nil", err)
	}
	if repo.saved == nil {
		t.Fatal("saved app is nil, want updated app")
	}
	got := repo.saved.NavigationItems
	if len(got) != 3 {
		t.Fatalf("saved navigation len = %d, want 3", len(got))
	}
	if got[0].Type != constants.NavigationTypeWeb || got[0].PageURL != "https://example.test/docs" || got[0].Icon != "Globe" {
		t.Fatalf("first nav = %+v, want preserved rich web nav", got[0])
	}
	if got[1].Type != constants.NavigationTypeObject || got[1].ObjectAPIName != "Account" || got[1].Icon != "Building2" {
		t.Fatalf("second nav = %+v, want preserved rich Account nav", got[1])
	}
	if got[2].Type != constants.NavigationTypePage || got[2].ObjectAPIName != "Home" || got[2].PageURL != "/" {
		t.Fatalf("third nav = %+v, want preserved rich Home nav", got[2])
	}
}

func TestUpdateAppWithPatchPresenceAndNavigationIdentitiesFailsClosedOnExistingReadError(t *testing.T) {
	repo := &metadataUpdateAppRepo{
		getErr: errors.New("transient read failed"),
	}
	ms := NewMetadataService(repo, nil, nil)

	err := ms.UpdateAppWithPatchPresenceAndNavigationIdentities(context.Background(), "Sales", &models.AppConfig{
		NavigationItems: []models.NavigationItem{
			{ID: "nav-0-account", Type: constants.NavigationTypeObject, ObjectAPIName: "Account", Label: "Accounts"},
		},
	}, false, []contract.ApplicationNavigationItemIdentity{
		{Kind: contract.ApplicationNavigationItemIdentityObject, Key: "Account"},
	})
	if err == nil {
		t.Fatal("UpdateAppWithPatchPresenceAndNavigationIdentities() error = nil, want fail-closed read error")
	}
	if repo.saved != nil {
		t.Fatalf("saved app = %+v, want no write on read failure", repo.saved)
	}
}
