package persistence

import (
	"context"
	"errors"
	"net/http"
	"testing"
	"time"

	"github.com/DATA-DOG/go-sqlmock"
	"github.com/nexuscrm/backend/pkg/apierr"
	"github.com/nexuscrm/shared/pkg/models"
)

func TestSetDefaultLayout_PersistsWildcardAssignmentAuthorshipWithoutPromotingLayout(t *testing.T) {
	db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	repo := NewMetadataRepository(db)
	userID := "005admin000000001"
	routeSource := models.LayoutAssignmentRouteSourceAdminAuthored

	mock.ExpectBegin()
	mock.ExpectQuery(`SELECT .*ObjectApiName.*FROM .*_System_PageLayout.*WHERE .*Id.*`).
		WithArgs("layout_default_choice").
		WillReturnRows(sqlmock.NewRows([]string{"ObjectApiName"}).AddRow("Opportunity"))
	mock.ExpectExec(`UPDATE .*_System_PageLayout.*SET .*IsDefault.*WHERE .*ObjectApiName.*`).
		WithArgs("Opportunity").
		WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectExec(`UPDATE .*_System_PageLayout.*SET .*IsDefault.*WHERE .*Id.*`).
		WithArgs("layout_default_choice", "Opportunity").
		WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectQuery(`SELECT .*ObjectApiName.*FROM .*_System_PageLayout.*WHERE .*Id.*`).
		WithArgs("layout_default_choice").
		WillReturnRows(sqlmock.NewRows([]string{"ObjectApiName"}).AddRow("Opportunity"))
	mock.ExpectExec(`UPDATE .*_System_LayoutAssignment.*SET .*LayoutId.*Specificity.*CreatedById.*RouteSource.*WHERE .*ObjectApiName.*RoleId.*IS NULL.*ProfileId.*IS NULL.*RecordTypeId.*IS NULL`).
		WithArgs("layout_default_choice", 0, userID, routeSource, "Opportunity").
		WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectCommit()

	if err := repo.SetDefaultLayout(context.Background(), "Opportunity", "layout_default_choice", &userID); err != nil {
		t.Fatalf("SetDefaultLayout() error = %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("expectations: %v", err)
	}
}

func TestCreateAssignment_UpdatesAssignmentAuthorshipWithoutPromotingLayout(t *testing.T) {
	db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	repo := NewMetadataRepository(db)
	userID := "005admin000000001"
	routeSource := models.LayoutAssignmentRouteSourceAdminAuthored
	assignment := &models.LayoutAssignment{
		ObjectAPIName: "Opportunity",
		LayoutID:      "layout_profile_choice",
		CreatedByID:   &userID,
		RouteSource:   &routeSource,
	}

	mock.ExpectBegin()
	mock.ExpectQuery(`SELECT .*ObjectApiName.*FROM .*_System_PageLayout.*WHERE .*Id.*`).
		WithArgs("layout_profile_choice").
		WillReturnRows(sqlmock.NewRows([]string{"ObjectApiName"}).AddRow("Opportunity"))
	mock.ExpectExec("UPDATE .*_System_LayoutAssignment.*CreatedById.*RouteSource.*RoleId.*IS NULL.*ProfileId.*IS NULL.*RecordTypeId.*IS NULL").
		WithArgs("layout_profile_choice", 0, userID, routeSource, "Opportunity").
		WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectCommit()

	if err := repo.CreateAssignment(context.Background(), assignment); err != nil {
		t.Fatalf("CreateAssignment() error = %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("expectations: %v", err)
	}
}

func TestCreateAssignment_CanonicalizesObjectAPINameFromTargetLayout(t *testing.T) {
	db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	repo := NewMetadataRepository(db)
	userID := "005admin000000001"
	routeSource := models.LayoutAssignmentRouteSourceAdminAuthored
	assignment := &models.LayoutAssignment{
		ObjectAPIName: "opportunity",
		LayoutID:      "layout_profile_choice",
		CreatedByID:   &userID,
		RouteSource:   &routeSource,
	}

	mock.ExpectBegin()
	mock.ExpectQuery(`SELECT .*ObjectApiName.*FROM .*_System_PageLayout.*WHERE .*Id.*`).
		WithArgs("layout_profile_choice").
		WillReturnRows(sqlmock.NewRows([]string{"ObjectApiName"}).AddRow("Opportunity"))
	mock.ExpectExec("UPDATE .*_System_LayoutAssignment.*CreatedById.*RouteSource.*RoleId.*IS NULL.*ProfileId.*IS NULL.*RecordTypeId.*IS NULL").
		WithArgs("layout_profile_choice", 0, userID, routeSource, "Opportunity").
		WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectCommit()

	if err := repo.CreateAssignment(context.Background(), assignment); err != nil {
		t.Fatalf("CreateAssignment() error = %v", err)
	}
	if assignment.ObjectAPIName != "Opportunity" {
		t.Fatalf("assignment.ObjectAPIName = %q, want canonical layout object name", assignment.ObjectAPIName)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("expectations: %v", err)
	}
}

func TestCreateAssignment_RejectsCrossObjectLayoutReference(t *testing.T) {
	db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	repo := NewMetadataRepository(db)
	userID := "005admin000000001"
	routeSource := models.LayoutAssignmentRouteSourceAdminAuthored
	assignment := &models.LayoutAssignment{
		ObjectAPIName: "User",
		LayoutID:      "layout_profile_choice",
		CreatedByID:   &userID,
		RouteSource:   &routeSource,
	}

	mock.ExpectBegin()
	mock.ExpectQuery(`SELECT .*ObjectApiName.*FROM .*_System_PageLayout.*WHERE .*Id.*`).
		WithArgs("layout_profile_choice").
		WillReturnRows(sqlmock.NewRows([]string{"ObjectApiName"}).AddRow("Opportunity"))
	mock.ExpectRollback()

	err = repo.CreateAssignment(context.Background(), assignment)
	if err == nil {
		t.Fatal("expected error, got nil")
	}

	var sfErr *apierr.SalesforceAPIError
	if !errors.As(err, &sfErr) {
		t.Fatalf("expected SalesforceAPIError, got %T (%v)", err, err)
	}
	if sfErr.Status != http.StatusBadRequest {
		t.Fatalf("status = %d, want %d", sfErr.Status, http.StatusBadRequest)
	}
	if sfErr.Err.ErrorCode != apierr.SFCodeInvalidCrossReferenceKey {
		t.Fatalf("errorCode = %q, want %q", sfErr.Err.ErrorCode, apierr.SFCodeInvalidCrossReferenceKey)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("expectations: %v", err)
	}
}

func TestCreateAssignment_RejectsUnstampedRouteSource(t *testing.T) {
	db, _, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	repo := NewMetadataRepository(db)
	assignment := &models.LayoutAssignment{
		ObjectAPIName: "Opportunity",
		LayoutID:      "layout_profile_choice",
	}

	err = repo.CreateAssignment(context.Background(), assignment)
	if err == nil {
		t.Fatal("expected error, got nil")
	}
	if err.Error() != "routeSource is required" {
		t.Fatalf("expected routeSource error, got %v", err)
	}
}

func TestCreateAssignment_RuntimeFallbackDoesNotOverwriteAdminAuthoredWildcard(t *testing.T) {
	db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	repo := NewMetadataRepository(db)
	routeSource := models.LayoutAssignmentRouteSourceRuntimeFallback
	assignment := &models.LayoutAssignment{
		ObjectAPIName: "Opportunity",
		LayoutID:      "layout_runtime_fallback",
		RouteSource:   &routeSource,
	}

	mock.ExpectBegin()
	mock.ExpectQuery(`SELECT .*ObjectApiName.*FROM .*_System_PageLayout.*WHERE .*Id.*`).
		WithArgs("layout_runtime_fallback").
		WillReturnRows(sqlmock.NewRows([]string{"ObjectApiName"}).AddRow("Opportunity"))
	mock.ExpectQuery("SELECT .*Id.*LayoutId.*RouteSource.*FROM .*_System_LayoutAssignment.*LOWER.*ObjectApiName.*LOWER").
		WithArgs("Opportunity").
		WillReturnRows(sqlmock.NewRows([]string{"Id", "LayoutId", "RouteSource"}).
			AddRow("la_admin_default", "layout_admin_default", models.LayoutAssignmentRouteSourceAdminAuthored))
	mock.ExpectCommit()

	if err := repo.CreateAssignment(context.Background(), assignment); err != nil {
		t.Fatalf("CreateAssignment() error = %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("expectations: %v", err)
	}
}

func TestCreateAssignment_RuntimeFallbackCanRepairRuntimeFallbackWildcard(t *testing.T) {
	db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	repo := NewMetadataRepository(db)
	routeSource := models.LayoutAssignmentRouteSourceRuntimeFallback
	assignment := &models.LayoutAssignment{
		ObjectAPIName: "Opportunity",
		LayoutID:      "layout_runtime_fallback",
		RouteSource:   &routeSource,
	}

	mock.ExpectBegin()
	mock.ExpectQuery(`SELECT .*ObjectApiName.*FROM .*_System_PageLayout.*WHERE .*Id.*`).
		WithArgs("layout_runtime_fallback").
		WillReturnRows(sqlmock.NewRows([]string{"ObjectApiName"}).AddRow("Opportunity"))
	mock.ExpectQuery("SELECT .*Id.*LayoutId.*RouteSource.*FROM .*_System_LayoutAssignment.*LOWER.*ObjectApiName.*LOWER").
		WithArgs("Opportunity").
		WillReturnRows(sqlmock.NewRows([]string{"Id", "LayoutId", "RouteSource"}).
			AddRow("la_runtime_default", "missing_runtime_layout", models.LayoutAssignmentRouteSourceRuntimeFallback))
	mock.ExpectExec(`UPDATE .*_System_LayoutAssignment.*SET .*LayoutId.*Specificity.*RouteSource.*WHERE .*Id`).
		WithArgs("layout_runtime_fallback", 0, routeSource, "la_runtime_default").
		WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectCommit()

	if err := repo.CreateAssignment(context.Background(), assignment); err != nil {
		t.Fatalf("CreateAssignment() error = %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("expectations: %v", err)
	}
}

func TestGetLayout_ProjectsEffectiveDefaultFromWildcardAssignment(t *testing.T) {
	db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	repo := NewMetadataRepository(db)
	now := time.Now()

	mock.ExpectQuery(`SELECT .* FROM .*_System_PageLayout.*WHERE .*Id.*`).
		WithArgs("layout_default_choice", false).
		WillReturnRows(sqlmock.NewRows([]string{
			"Id", "ObjectApiName", "LayoutName", "Type", "IsDefault", "IsDeleted", "Config",
			"CreatedDate", "LastModifiedDate", "IsCustomized", "LayoutVersion", "Source", "GenerationVersion",
		}).AddRow(
			"layout_default_choice", "Opportunity", "Opportunity Layout", "Detail", false, false, `{}`,
			now, now, false, 0, "auto_generated", 8,
		))
	mock.ExpectQuery(`SELECT la\.` + "`Id`" + `, la\.` + "`LayoutId`" + `.*FROM .*_System_LayoutAssignment.*RoleId.*IS NULL.*ProfileId.*IS NULL.*RecordTypeId.*IS NULL`).
		WithArgs("Opportunity").
		WillReturnRows(sqlmock.NewRows([]string{"Id", "LayoutId", "Source", "IsCustomized", "TargetPresent", "RouteSource"}).
			AddRow("la_default", "layout_default_choice", "auto_generated", false, true, models.LayoutAssignmentRouteSourceAdminAuthored))

	layout, err := repo.GetLayout(context.Background(), "layout_default_choice")
	if err != nil {
		t.Fatalf("GetLayout() error = %v", err)
	}
	if layout == nil {
		t.Fatal("expected layout, got nil")
	}
	if !layout.IsDefault {
		t.Fatal("expected projected default layout")
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("expectations: %v", err)
	}
}

func TestGetLayouts_ProjectsEffectiveDefaultFromWildcardAssignment(t *testing.T) {
	db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	repo := NewMetadataRepository(db)
	now := time.Now()

	mock.ExpectQuery(`SELECT .* FROM .*_System_PageLayout.*WHERE .*ObjectApiName.*`).
		WithArgs("Create", "Opportunity", false).
		WillReturnRows(sqlmock.NewRows([]string{
			"Id", "ObjectApiName", "LayoutName", "Type", "IsDefault", "IsDeleted", "Config",
			"CreatedDate", "LastModifiedDate", "IsCustomized", "LayoutVersion", "Source", "GenerationVersion",
		}).
			AddRow("layout_other", "Opportunity", "Other Layout", "Detail", true, false, `{}`, now, now, false, 0, "auto_generated", 8).
			AddRow("layout_default_choice", "Opportunity", "Opportunity Layout", "Detail", false, false, `{}`, now, now, false, 0, "auto_generated", 8))
	mock.ExpectQuery(`SELECT la\.` + "`Id`" + `, la\.` + "`LayoutId`" + `.*FROM .*_System_LayoutAssignment.*RoleId.*IS NULL.*ProfileId.*IS NULL.*RecordTypeId.*IS NULL`).
		WithArgs("Opportunity").
		WillReturnRows(sqlmock.NewRows([]string{"Id", "LayoutId", "Source", "IsCustomized", "TargetPresent", "RouteSource"}).
			AddRow("la_default", "layout_default_choice", "auto_generated", false, true, models.LayoutAssignmentRouteSourceAdminAuthored))

	layouts, err := repo.GetLayouts(context.Background(), "Opportunity")
	if err != nil {
		t.Fatalf("GetLayouts() error = %v", err)
	}
	if len(layouts) != 2 {
		t.Fatalf("expected 2 layouts, got %d", len(layouts))
	}
	if layouts[0].ID != "layout_default_choice" || !layouts[0].IsDefault {
		t.Fatalf("expected projected default first, got %#v", layouts[0])
	}
	if layouts[1].IsDefault {
		t.Fatalf("expected non-default second layout, got %#v", layouts[1])
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("expectations: %v", err)
	}
}
