package metadata

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

	apperrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

func TestListViewServiceCreatePrivateStampsCallerOwnerAndGeneratedDeveloperName(t *testing.T) {
	repo := newListViewServiceRepoStub()
	perms := &listViewPermissionStub{allow: true}
	svc := NewListViewService(repo, perms)

	spoofedOwner := "005-other"
	got, err := svc.CreatePrivate(context.Background(), &models.UserSession{ID: "005-owner"}, &models.ListView{
		ObjectAPIName: "Account",
		Label:         "My Accounts",
		Fields:        []string{"Name", "OwnerId"},
		OwnerID:       &spoofedOwner,
	})
	if err != nil {
		t.Fatalf("CreatePrivate() error = %v, want nil", err)
	}
	if got == nil || got.ID == "" {
		t.Fatalf("CreatePrivate() returned %#v, want generated ID", got)
	}
	if repo.created == nil {
		t.Fatal("CreatePrivate() did not persist a row")
	}
	if repo.created.OwnerID == nil || *repo.created.OwnerID != "005-owner" {
		t.Fatalf("created OwnerID = %#v, want caller owner", repo.created.OwnerID)
	}
	if repo.created.Visibility == nil || *repo.created.Visibility != "Private" {
		t.Fatalf("created Visibility = %#v, want Private", repo.created.Visibility)
	}
	if repo.created.DeveloperName == nil || *repo.created.DeveloperName != "My_Accounts" {
		t.Fatalf("created DeveloperName = %#v, want generated My_Accounts", repo.created.DeveloperName)
	}
	if len(perms.calls) != 1 || perms.calls[0] != string(constants.PermCreateCustomizeFilters) {
		t.Fatalf("permission calls = %#v, want CreateCustomizeFilters only", perms.calls)
	}
}

func TestListViewServiceReadsOnlyCallerOwnedPrivateRows(t *testing.T) {
	repo := newListViewServiceRepoStub()
	owner, other, private, shared := "user-1", "user-2", "Private", "Shared"
	repo.rows["owned"] = &models.ListView{ID: "owned", ObjectAPIName: "Account", Label: "Owned", OwnerID: &owner, Visibility: &private}
	repo.rows["foreign"] = &models.ListView{ID: "foreign", ObjectAPIName: "Account", Label: "Foreign", OwnerID: &other, Visibility: &private}
	repo.rows["shared"] = &models.ListView{ID: "shared", ObjectAPIName: "Account", Label: "Shared", OwnerID: &owner, Visibility: &shared}
	service := NewListViewService(repo, &listViewPermissionStub{allow: true})
	user := &models.UserSession{ID: owner}

	views, err := service.ListPrivate(context.Background(), user, "Account")
	if err != nil || len(views) != 1 || views[0].ID != "owned" {
		t.Fatalf("ListPrivate() = %#v, %v; want only owned private row", views, err)
	}
	if _, err := service.DescribePrivate(context.Background(), user, "Account", "foreign"); !apperrors.IsNotFound(err) {
		t.Fatalf("DescribePrivate(foreign) error = %v, want non-disclosing not found", err)
	}
	if _, err := service.DescribePrivate(context.Background(), user, "Contact", "owned"); !apperrors.IsNotFound(err) {
		t.Fatalf("DescribePrivate(wrong object) error = %v, want non-disclosing not found", err)
	}
	view, err := service.DescribePrivate(context.Background(), user, "Account", "owned")
	if err != nil || view.ID != "owned" {
		t.Fatalf("DescribePrivate(owned) = %#v, %v", view, err)
	}
}

func TestListViewServiceCreatePrivateNormalizesOmittedFieldsToEmptySlice(t *testing.T) {
	repo := newListViewServiceRepoStub()
	svc := NewListViewService(repo, &listViewPermissionStub{allow: true})

	got, err := svc.CreatePrivate(context.Background(), &models.UserSession{ID: "005-owner"}, &models.ListView{
		ObjectAPIName: "Account",
		Label:         "My Accounts",
	})
	if err != nil {
		t.Fatalf("CreatePrivate(omitted Fields) error = %v, want nil", err)
	}
	if got.Fields == nil {
		t.Fatal("CreatePrivate(omitted Fields) returned nil Fields, want explicit empty slice")
	}
	if repo.created == nil || repo.created.Fields == nil {
		t.Fatalf("persisted Fields = %#v, want explicit empty slice", repo.created)
	}
	if len(repo.created.Fields) != 0 {
		t.Fatalf("persisted Fields = %#v, want empty slice", repo.created.Fields)
	}
}

func TestListViewServiceCreatePrivateIgnoresCallerSuppliedReservedID(t *testing.T) {
	repo := newListViewServiceRepoStub()
	svc := NewListViewService(repo, &listViewPermissionStub{allow: true})

	got, err := svc.CreatePrivate(context.Background(), &models.UserSession{ID: "005-owner"}, &models.ListView{
		ID:            "Account-all-view",
		ObjectAPIName: "Account",
		Label:         "My Accounts",
		Fields:        []string{"Name"},
	})
	if err != nil {
		t.Fatalf("CreatePrivate(caller ID) error = %v, want nil", err)
	}
	if got.ID == "" || got.ID == "Account-all-view" {
		t.Fatalf("CreatePrivate() ID = %q, want generated non-reserved ID", got.ID)
	}
	if repo.created == nil || repo.created.ID == "Account-all-view" {
		t.Fatalf("persisted ID = %#v, want generated non-reserved ID", repo.created)
	}
}

func TestListViewServiceCreatePrivateRejectsPermissionDeniedBeforeWrite(t *testing.T) {
	repo := newListViewServiceRepoStub()
	svc := NewListViewService(repo, &listViewPermissionStub{allow: false})

	_, err := svc.CreatePrivate(context.Background(), &models.UserSession{ID: "005-owner"}, &models.ListView{
		ObjectAPIName: "Account",
		Label:         "My Accounts",
	})
	if !apperrors.IsPermission(err) {
		t.Fatalf("CreatePrivate() error = %T %v, want PermissionError", err, err)
	}
	if repo.created != nil {
		t.Fatalf("CreatePrivate() wrote row despite permission denial: %#v", repo.created)
	}
}

func TestListViewServiceUpdatePrivateDeniesNonOwnerWithoutWriting(t *testing.T) {
	ownerID := "005-owner"
	visibility := "Private"
	developerName := "My_Accounts"
	repo := newListViewServiceRepoStub()
	repo.rows["lv-1"] = &models.ListView{
		ID:            "lv-1",
		ObjectAPIName: "Account",
		Label:         "My Accounts",
		FilterExpr:    "Name != ''",
		OwnerID:       &ownerID,
		Visibility:    &visibility,
		DeveloperName: &developerName,
	}
	svc := NewListViewService(repo, &listViewPermissionStub{allow: true})

	newLabel := "Renamed"
	_, err := svc.UpdatePrivate(context.Background(), &models.UserSession{ID: "005-other"}, "lv-1", ListViewPatch{
		Label: &newLabel,
	})
	if !apperrors.IsNotFound(err) {
		t.Fatalf("UpdatePrivate() error = %T %v, want non-leaking NotFoundError", err, err)
	}
	if repo.updated != nil {
		t.Fatalf("UpdatePrivate() wrote row despite non-owner denial: %#v", repo.updated)
	}
}

func TestListViewServiceRejectsPublicAndSharedRowsFailClosed(t *testing.T) {
	ownerID := "005-owner"
	publicVisibility := "Public"
	sharedVisibility := "Shared"
	repo := newListViewServiceRepoStub()
	repo.rows["lv-public"] = &models.ListView{ID: "lv-public", ObjectAPIName: "Account", Label: "Public", OwnerID: &ownerID, Visibility: &publicVisibility}
	repo.rows["lv-shared"] = &models.ListView{ID: "lv-shared", ObjectAPIName: "Account", Label: "Shared", OwnerID: &ownerID, Visibility: &sharedVisibility}
	svc := NewListViewService(repo, &listViewPermissionStub{allow: true})

	label := "Renamed"
	_, updateErr := svc.UpdatePrivate(context.Background(), &models.UserSession{ID: ownerID}, "lv-public", ListViewPatch{Label: &label})
	var updateForbidden *apperrors.ForbiddenError
	if !stderrors.As(updateErr, &updateForbidden) {
		t.Fatalf("UpdatePrivate(public) error = %T %v, want ForbiddenError", updateErr, updateErr)
	}
	deleteErr := svc.DeletePrivate(context.Background(), &models.UserSession{ID: ownerID}, "lv-shared")
	var deleteForbidden *apperrors.ForbiddenError
	if !stderrors.As(deleteErr, &deleteForbidden) {
		t.Fatalf("DeletePrivate(shared) error = %T %v, want ForbiddenError", deleteErr, deleteErr)
	}
	if repo.updated != nil || repo.deletedID != "" {
		t.Fatalf("Public/Shared rejection performed writes: updated=%#v deleted=%q", repo.updated, repo.deletedID)
	}
}

func TestListViewServiceDeveloperNameCollisionDoesNotLeakHiddenRow(t *testing.T) {
	hiddenOwner := "005-other"
	hiddenVisibility := "Private"
	hiddenDeveloperName := "My_Accounts"
	repo := newListViewServiceRepoStub()
	repo.developerNameMatch = &models.ListView{
		ID:            "lv-hidden",
		ObjectAPIName: "Account",
		Label:         "Secret Owner View",
		OwnerID:       &hiddenOwner,
		Visibility:    &hiddenVisibility,
		DeveloperName: &hiddenDeveloperName,
	}
	svc := NewListViewService(repo, &listViewPermissionStub{allow: true})

	_, err := svc.CreatePrivate(context.Background(), &models.UserSession{ID: "005-owner"}, &models.ListView{
		ObjectAPIName: "Account",
		Label:         "My Accounts",
	})
	if !apperrors.IsConflict(err) {
		t.Fatalf("CreatePrivate() error = %T %v, want ConflictError", err, err)
	}
	errText := err.Error()
	for _, forbidden := range []string{"005-other", "Secret Owner View", "lv-hidden"} {
		if strings.Contains(errText, forbidden) {
			t.Fatalf("collision error %q leaked hidden row detail %q", errText, forbidden)
		}
	}
}

func TestListViewServiceUpdatePrivateFilterExprPatchPresence(t *testing.T) {
	ownerID := "005-owner"
	visibility := "Private"
	developerName := "My_Accounts"
	repo := newListViewServiceRepoStub()
	repo.rows["lv-1"] = &models.ListView{
		ID:            "lv-1",
		ObjectAPIName: "Account",
		Label:         "My Accounts",
		FilterExpr:    "Name != ''",
		Fields:        []string{"Name"},
		OwnerID:       &ownerID,
		Visibility:    &visibility,
		DeveloperName: &developerName,
	}
	svc := NewListViewService(repo, &listViewPermissionStub{allow: true})

	newLabel := "Renamed"
	got, err := svc.UpdatePrivate(context.Background(), &models.UserSession{ID: ownerID}, "lv-1", ListViewPatch{
		Label: &newLabel,
	})
	if err != nil {
		t.Fatalf("UpdatePrivate(omitted FilterExpr) error = %v", err)
	}
	if got.FilterExpr != "Name != ''" || repo.updated.FilterExpr != "Name != ''" {
		t.Fatalf("omitted FilterExpr got=%q updated=%q, want preserved", got.FilterExpr, repo.updated.FilterExpr)
	}

	repo.updated = nil
	emptyFilter := ""
	got, err = svc.UpdatePrivate(context.Background(), &models.UserSession{ID: ownerID}, "lv-1", ListViewPatch{
		FilterExpr: &emptyFilter,
	})
	if err != nil {
		t.Fatalf("UpdatePrivate(empty FilterExpr) error = %v", err)
	}
	if got.FilterExpr != "" || repo.updated.FilterExpr != "" {
		t.Fatalf("explicit empty FilterExpr got=%q updated=%q, want cleared", got.FilterExpr, repo.updated.FilterExpr)
	}
}

func TestListViewServiceUpdatePrivateDefinitionPatchPresence(t *testing.T) {
	ownerID := "005-owner"
	visibility := "Private"
	developerName := "My_Accounts"
	repo := newListViewServiceRepoStub()
	repo.rows["lv-1"] = &models.ListView{
		ID:            "lv-1",
		ObjectAPIName: "Account",
		Label:         "My Accounts",
		FilterLogic:   "1 AND 2",
		SortField:     "Name",
		SortDirection: "ASC",
		Fields:        []string{"Name"},
		OwnerID:       &ownerID,
		Visibility:    &visibility,
		DeveloperName: &developerName,
	}
	svc := NewListViewService(repo, &listViewPermissionStub{allow: true})

	sortDirection := "DESC"
	got, err := svc.UpdatePrivate(context.Background(), &models.UserSession{ID: ownerID}, "lv-1", ListViewPatch{
		SortDirection: &sortDirection,
	})
	if err != nil {
		t.Fatalf("UpdatePrivate(sort direction) error = %v", err)
	}
	if got.FilterLogic != "1 AND 2" || got.SortField != "Name" || got.SortDirection != "DESC" {
		t.Fatalf("definition patch got filterLogic=%q sort=%q/%q", got.FilterLogic, got.SortField, got.SortDirection)
	}
	if repo.updated == nil || repo.updated.FilterLogic != "1 AND 2" || repo.updated.SortField != "Name" || repo.updated.SortDirection != "DESC" {
		t.Fatalf("definition patch persisted %#v", repo.updated)
	}

	repo.updated = nil
	clearedLogic := ""
	got, err = svc.UpdatePrivate(context.Background(), &models.UserSession{ID: ownerID}, "lv-1", ListViewPatch{
		FilterLogic: &clearedLogic,
	})
	if err != nil {
		t.Fatalf("UpdatePrivate(clear filter logic) error = %v", err)
	}
	if got.FilterLogic != "" || repo.updated.FilterLogic != "" {
		t.Fatalf("explicit empty FilterLogic got=%q updated=%q, want cleared", got.FilterLogic, repo.updated.FilterLogic)
	}
}

func TestListViewServiceUpdatePrivatePreservesConcurrentDisjointPatchFields(t *testing.T) {
	ownerID := "005-owner"
	visibility := "Private"
	developerName := "My_Accounts"
	repo := newListViewServiceRepoStub()
	repo.rows["lv-1"] = &models.ListView{
		ID:            "lv-1",
		ObjectAPIName: "Account",
		Label:         "My Accounts",
		FilterExpr:    "Name != ''",
		Fields:        []string{"Name"},
		OwnerID:       &ownerID,
		Visibility:    &visibility,
		DeveloperName: &developerName,
	}
	repo.afterGetListView = func(id string, count int) {
		if id == "lv-1" && count == 1 {
			repo.rows[id].FilterExpr = "Rating = 'Hot'"
		}
	}
	svc := NewListViewService(repo, &listViewPermissionStub{allow: true})

	newLabel := "Renamed"
	got, err := svc.UpdatePrivate(context.Background(), &models.UserSession{ID: ownerID}, "lv-1", ListViewPatch{
		Label: &newLabel,
	})
	if err != nil {
		t.Fatalf("UpdatePrivate(label-only) error = %v", err)
	}
	if got.Label != "Renamed" || repo.rows["lv-1"].Label != "Renamed" {
		t.Fatalf("label patch got=%q persisted=%q, want Renamed", got.Label, repo.rows["lv-1"].Label)
	}
	if got.FilterExpr != "Rating = 'Hot'" || repo.rows["lv-1"].FilterExpr != "Rating = 'Hot'" {
		t.Fatalf("label-only patch rewrote stale FilterExpr: got=%q persisted=%q", got.FilterExpr, repo.rows["lv-1"].FilterExpr)
	}
}

func TestListViewServiceUpdatePrivateNormalizesExplicitEmptyFieldsPatch(t *testing.T) {
	ownerID := "005-owner"
	visibility := "Private"
	developerName := "My_Accounts"
	repo := newListViewServiceRepoStub()
	repo.rows["lv-1"] = &models.ListView{
		ID:            "lv-1",
		ObjectAPIName: "Account",
		Label:         "My Accounts",
		Fields:        []string{"Name"},
		OwnerID:       &ownerID,
		Visibility:    &visibility,
		DeveloperName: &developerName,
	}
	svc := NewListViewService(repo, &listViewPermissionStub{allow: true})

	emptyFields := []string{}
	got, err := svc.UpdatePrivate(context.Background(), &models.UserSession{ID: ownerID}, "lv-1", ListViewPatch{
		Fields: &emptyFields,
	})
	if err != nil {
		t.Fatalf("UpdatePrivate(empty Fields) error = %v", err)
	}
	if got.Fields == nil {
		t.Fatal("UpdatePrivate(empty Fields) returned nil Fields, want explicit empty slice")
	}
	if repo.updated == nil || repo.updated.Fields == nil {
		t.Fatalf("persisted Fields = %#v, want explicit empty slice", repo.updated)
	}
	if len(got.Fields) != 0 || len(repo.updated.Fields) != 0 {
		t.Fatalf("empty Fields patch got=%#v updated=%#v, want empty slices", got.Fields, repo.updated.Fields)
	}
}

type listViewServiceRepoStub struct {
	rows               map[string]*models.ListView
	created            *models.ListView
	updatedID          string
	updated            *models.ListView
	patchedID          string
	patchedOwnerID     string
	patched            ListViewPatch
	deletedID          string
	developerNameMatch *models.ListView
	getListViewCount   int
	afterGetListView   func(id string, count int)
}

func newListViewServiceRepoStub() *listViewServiceRepoStub {
	return &listViewServiceRepoStub{rows: map[string]*models.ListView{}}
}

func (r *listViewServiceRepoStub) CreateListView(ctx context.Context, view *models.ListView) error {
	r.created = cloneListViewForTest(view)
	r.rows[view.ID] = cloneListViewForTest(view)
	return nil
}

func (r *listViewServiceRepoStub) GetPrivateListViews(_ context.Context, objectAPIName string, ownerID string) ([]*models.ListView, error) {
	result := []*models.ListView{}
	for _, view := range r.rows {
		if view.ObjectAPIName == objectAPIName && view.OwnerID != nil && *view.OwnerID == ownerID &&
			view.Visibility != nil && isPrivateVisibility(*view.Visibility) {
			result = append(result, cloneListViewForTest(view))
		}
	}
	return result, nil
}

func (r *listViewServiceRepoStub) GetPrivateListView(_ context.Context, objectAPIName string, id string, ownerID string) (*models.ListView, error) {
	view, ok := r.rows[id]
	if !ok || view.ObjectAPIName != objectAPIName || view.OwnerID == nil || *view.OwnerID != ownerID ||
		view.Visibility == nil || !isPrivateVisibility(*view.Visibility) {
		return nil, sql.ErrNoRows
	}
	return cloneListViewForTest(view), nil
}

func (r *listViewServiceRepoStub) UpdateListViewPatch(ctx context.Context, id string, ownerID string, patch ListViewPatch) error {
	view, ok := r.rows[id]
	if !ok {
		return sql.ErrNoRows
	}
	if view.OwnerID == nil || strings.TrimSpace(*view.OwnerID) != strings.TrimSpace(ownerID) ||
		view.Visibility == nil || !isPrivateVisibility(*view.Visibility) {
		return sql.ErrNoRows
	}

	updated := cloneListViewForTest(view)
	if patch.Label != nil {
		updated.Label = *patch.Label
	}
	if patch.FilterExpr != nil {
		updated.FilterExpr = *patch.FilterExpr
	}
	if patch.FilterLogic != nil {
		updated.FilterLogic = *patch.FilterLogic
	}
	if patch.SortField != nil {
		updated.SortField = *patch.SortField
	}
	if patch.SortDirection != nil {
		updated.SortDirection = *patch.SortDirection
	}
	if patch.Fields != nil {
		updated.Fields = copyStringSliceForTest(*patch.Fields)
	}
	if patch.DeveloperName != nil {
		developerName := *patch.DeveloperName
		updated.DeveloperName = &developerName
	}

	r.patchedID = id
	r.patchedOwnerID = ownerID
	r.patched = cloneListViewPatchForTest(patch)
	r.updatedID = id
	r.updated = cloneListViewForTest(updated)
	r.rows[id] = cloneListViewForTest(updated)
	return nil
}

func (r *listViewServiceRepoStub) DeletePrivateListView(ctx context.Context, id string, ownerID string) error {
	view, ok := r.rows[id]
	if !ok {
		return sql.ErrNoRows
	}
	if view.OwnerID == nil || strings.TrimSpace(*view.OwnerID) != strings.TrimSpace(ownerID) ||
		view.Visibility == nil || !isPrivateVisibility(*view.Visibility) {
		return sql.ErrNoRows
	}
	r.deletedID = id
	delete(r.rows, id)
	return nil
}

func (r *listViewServiceRepoStub) GetListView(ctx context.Context, id string) (*models.ListView, error) {
	view, ok := r.rows[id]
	if !ok {
		return nil, sql.ErrNoRows
	}
	out := cloneListViewForTest(view)
	r.getListViewCount++
	if r.afterGetListView != nil {
		r.afterGetListView(id, r.getListViewCount)
	}
	return out, nil
}

func (r *listViewServiceRepoStub) GetListViewForUpdate(ctx context.Context, id string) (*models.ListView, error) {
	return r.GetListView(ctx, id)
}

func (r *listViewServiceRepoStub) GetListViewByDeveloperName(ctx context.Context, objectAPIName, developerName string) (*models.ListView, error) {
	if r.developerNameMatch == nil {
		return nil, sql.ErrNoRows
	}
	return cloneListViewForTest(r.developerNameMatch), nil
}

func (r *listViewServiceRepoStub) GetListViewByDeveloperNameForUpdate(ctx context.Context, objectAPIName, developerName string) (*models.ListView, error) {
	return r.GetListViewByDeveloperName(ctx, objectAPIName, developerName)
}

type listViewPermissionStub struct {
	allow bool
	calls []string
}

func (p *listViewPermissionStub) HasPermission(ctx context.Context, user *models.UserSession, apiName string) bool {
	p.calls = append(p.calls, apiName)
	return p.allow
}

func cloneListViewForTest(view *models.ListView) *models.ListView {
	if view == nil {
		return nil
	}
	out := *view
	out.Fields = copyStringSliceForTest(view.Fields)
	if view.OwnerID != nil {
		owner := *view.OwnerID
		out.OwnerID = &owner
	}
	if view.Visibility != nil {
		visibility := *view.Visibility
		out.Visibility = &visibility
	}
	if view.DeveloperName != nil {
		developerName := *view.DeveloperName
		out.DeveloperName = &developerName
	}
	return &out
}

func copyStringSliceForTest(values []string) []string {
	if values == nil {
		return nil
	}
	return append([]string{}, values...)
}

func cloneListViewPatchForTest(patch ListViewPatch) ListViewPatch {
	out := ListViewPatch{}
	if patch.Label != nil {
		label := *patch.Label
		out.Label = &label
	}
	if patch.FilterExpr != nil {
		filterExpr := *patch.FilterExpr
		out.FilterExpr = &filterExpr
	}
	if patch.FilterLogic != nil {
		filterLogic := *patch.FilterLogic
		out.FilterLogic = &filterLogic
	}
	if patch.SortField != nil {
		sortField := *patch.SortField
		out.SortField = &sortField
	}
	if patch.SortDirection != nil {
		sortDirection := *patch.SortDirection
		out.SortDirection = &sortDirection
	}
	if patch.Fields != nil {
		fields := copyStringSliceForTest(*patch.Fields)
		out.Fields = &fields
	}
	if patch.DeveloperName != nil {
		developerName := *patch.DeveloperName
		out.DeveloperName = &developerName
	}
	return out
}
