package analytics

import (
	"context"
	"errors"
	"regexp"
	"testing"

	"github.com/DATA-DOG/go-sqlmock"
	pkgerrors "github.com/nexuscrm/backend/pkg/errors"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func newTestUser() *models.UserSession {
	return &models.UserSession{ID: "005test", ProfileID: "sys"}
}

// newMockLoader wires a crtLoader against a fresh sqlmock.
func newMockLoader(t *testing.T) (CRTRepository, sqlmock.Sqlmock, func()) {
	t.Helper()
	db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
	require.NoError(t, err)
	return NewCRTLoader(db, nil), mock, func() { _ = db.Close() }
}

var crtRowColumns = []string{
	"Id", "FullName", "Label", "BaseObject",
	"Deployed", "Autogenerated",
	"Sections", "Join", "Metadata", "Description",
}

func rtRows(fullName, baseObj string, deployed int) *sqlmock.Rows {
	return sqlmock.NewRows(crtRowColumns).AddRow(
		"rt-"+fullName, fullName, fullName+" label", baseObj,
		deployed, 0,
		`[{"columns":[{"field":"Name","table":"`+baseObj+`"}]}]`,
		`{"outerJoin":"false","relationship":"X"}`,
		"{}",
		"",
	)
}

func TestCRTLoader_LoadByFullName_HappyPath(t *testing.T) {
	loader, mock, cleanup := newMockLoader(t)
	defer cleanup()
	mock.ExpectQuery(regexp.QuoteMeta("FullName` = ? AND `Deployed` = 1")).
		WithArgs("Campaigns_with_Members__c").
		WillReturnRows(rtRows("Campaigns_with_Members__c", "Campaign", 1))

	got, err := loader.LoadByFullName(context.Background(), "Campaigns_with_Members__c", newTestUser())
	require.NoError(t, err)
	require.NotNil(t, got)
	assert.Equal(t, "Campaigns_with_Members__c", got.FullName)
	assert.Equal(t, "Campaign", got.BaseObject)
	assert.True(t, got.Deployed)
	assert.Contains(t, got.Sections, "Name")
	require.NoError(t, mock.ExpectationsWereMet())
}

func TestCRTLoader_LoadByFullName_TrimsCustomSuffix(t *testing.T) {
	loader, mock, cleanup := newMockLoader(t)
	defer cleanup()
	// Verbatim query returns nothing…
	mock.ExpectQuery("FullName").WithArgs("MyRT__c").WillReturnRows(sqlmock.NewRows(crtRowColumns))
	// …second query against trimmed FullName succeeds.
	mock.ExpectQuery("FullName").WithArgs("MyRT").WillReturnRows(rtRows("MyRT", "Account", 1))

	got, err := loader.LoadByFullName(context.Background(), "MyRT__c", newTestUser())
	require.NoError(t, err)
	require.NotNil(t, got)
	assert.Equal(t, "MyRT", got.FullName)
	require.NoError(t, mock.ExpectationsWereMet())
}

func TestCRTLoader_LoadByFullName_DeployedFlagFilters(t *testing.T) {
	loader, mock, cleanup := newMockLoader(t)
	defer cleanup()
	// Verify the SQL carries Deployed = 1 literal, and empty rows → NotFound.
	sqlRegex := regexp.QuoteMeta("Deployed` = 1")
	mock.ExpectQuery(sqlRegex).WithArgs("Unpublished__c").WillReturnRows(sqlmock.NewRows(crtRowColumns))
	mock.ExpectQuery(sqlRegex).WithArgs("Unpublished").WillReturnRows(sqlmock.NewRows(crtRowColumns))

	_, err := loader.LoadByFullName(context.Background(), "Unpublished__c", newTestUser())
	require.Error(t, err)
	var nf *pkgerrors.NotFoundError
	assert.True(t, errors.As(err, &nf))
	require.NoError(t, mock.ExpectationsWereMet())
}

func TestCRTLoader_LoadByFullName_NotFound(t *testing.T) {
	loader, mock, cleanup := newMockLoader(t)
	defer cleanup()
	mock.ExpectQuery("FullName").WithArgs("Missing").WillReturnRows(sqlmock.NewRows(crtRowColumns))

	_, err := loader.LoadByFullName(context.Background(), "Missing", newTestUser())
	require.Error(t, err)
	var nf *pkgerrors.NotFoundError
	assert.True(t, errors.As(err, &nf))
}

func TestCRTLoader_LoadByFullName_InfrastructureError(t *testing.T) {
	loader, mock, cleanup := newMockLoader(t)
	defer cleanup()
	mock.ExpectQuery("FullName").WithArgs("Boom").WillReturnError(errors.New("database unreachable"))

	_, err := loader.LoadByFullName(context.Background(), "Boom", newTestUser())
	require.Error(t, err)
	assert.Contains(t, err.Error(), "load CRT by full name")
	assert.Contains(t, err.Error(), "database unreachable")
}

func TestCRTLoader_LoadByID_HappyPath(t *testing.T) {
	loader, mock, cleanup := newMockLoader(t)
	defer cleanup()
	mock.ExpectQuery(regexp.QuoteMeta("`Id` = ?")).WithArgs("rt-123").WillReturnRows(rtRows("X", "Lead", 1))

	got, err := loader.LoadByID(context.Background(), "rt-123", newTestUser())
	require.NoError(t, err)
	assert.Equal(t, "X", got.FullName)
}

func TestCRTLoader_LoadByID_NotFound(t *testing.T) {
	loader, mock, cleanup := newMockLoader(t)
	defer cleanup()
	mock.ExpectQuery("Id").WithArgs("missing").WillReturnRows(sqlmock.NewRows(crtRowColumns))

	_, err := loader.LoadByID(context.Background(), "missing", newTestUser())
	require.Error(t, err)
	var nf *pkgerrors.NotFoundError
	assert.True(t, errors.As(err, &nf))
}

func TestCRTLoader_EmptyInputs(t *testing.T) {
	loader, _, cleanup := newMockLoader(t)
	defer cleanup()
	_, err := loader.LoadByFullName(context.Background(), "   ", newTestUser())
	require.Error(t, err)
	_, err = loader.LoadByID(context.Background(), "", newTestUser())
	require.Error(t, err)
}
