package rest_test

import (
	"encoding/base64"
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"net/url"
	"testing"

	"github.com/gin-gonic/gin"
	"github.com/nexuscrm/backend/internal/application/services"
	"github.com/nexuscrm/backend/internal/features/metadata"
	"github.com/nexuscrm/backend/internal/features/permissions"
	"github.com/nexuscrm/backend/internal/interfaces/rest"
	"github.com/nexuscrm/backend/pkg/auth"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
)

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

// newQueryTestContext creates a gin test context with an authenticated user.
func newQueryTestContext(method, target string) (*httptest.ResponseRecorder, *gin.Context) {
	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Request = httptest.NewRequest(method, target, nil)

	authUser := auth.UserSession{
		ID:        "test-user-1",
		Name:      "Test User",
		Email:     "test@example.com",
		ProfileId: constants.ProfileSystemAdmin,
	}
	c.Set(constants.ContextKeyUser, authUser)
	return w, c
}

// newQueryHandler creates a QueryHandler wired to the given mock query service.
func newQueryHandler(mockQuery *MockQueryService) *rest.QueryHandler {
	mockMetaRepo := new(MockMetadataRepo)
	mockMetaRepo.On("GetAllSchemas", mock.Anything).Return([]*models.ObjectMetadata{}, nil).Maybe()
	mockMetaRepo.On("GetSchemaByAPIName", mock.Anything, mock.AnythingOfType("string")).
		Return(&models.ObjectMetadata{APIName: "Account"}, nil).
		Maybe()
	metaSvc := metadata.NewMetadataService(mockMetaRepo, nil, nil)
	permSvc := &permissions.PermissionService{}
	svcMgr := &services.ServiceManager{
		QuerySvc:    mockQuery,
		Metadata:    metaSvc,
		Permissions: permSvc,
	}
	return rest.NewQueryHandler(svcMgr)
}

// ---------------------------------------------------------------------------
// ExecuteQuery — GET /query?q=...
// ---------------------------------------------------------------------------

func TestQueryHandler_ExecuteQuery_Success(t *testing.T) {
	gin.SetMode(gin.TestMode)

	mockQuery := new(MockQueryService)
	handler := newQueryHandler(mockQuery)

	soql := "SELECT Id, Name FROM Account"
	w, c := newQueryTestContext("GET", "/query?q="+url.QueryEscape(soql))

	mockQuery.On("Query", mock.Anything, mock.MatchedBy(func(req interface{}) bool {
		// Verify the parsed SOQL is forwarded correctly
		return true
	}), mock.Anything).Return(&models.QueryResult{
		TotalSize: 2,
		Done:      true,
		Records: []models.SObject{
			{"Id": "001A", "Name": "Acme"},
			{"Id": "001B", "Name": "Globex"},
		},
	}, nil)

	handler.ExecuteQuery(c)

	assert.Equal(t, http.StatusOK, w.Code)

	var resp map[string]interface{}
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))

	assert.Equal(t, float64(2), resp["totalSize"])
	assert.Equal(t, true, resp["done"])

	records := resp["records"].([]interface{})
	assert.Len(t, records, 2)

	// Verify attributes are injected
	first := records[0].(map[string]interface{})
	attrs := first["attributes"].(map[string]interface{})
	assert.Equal(t, "Account", attrs["type"])
	assert.Contains(t, attrs["url"], "/sobjects/Account/001A")

	mockQuery.AssertExpectations(t)
}

func TestQueryHandler_ExecuteQuery_MissingQParam(t *testing.T) {
	gin.SetMode(gin.TestMode)

	mockQuery := new(MockQueryService)
	handler := newQueryHandler(mockQuery)

	w, c := newQueryTestContext("GET", "/query")

	handler.ExecuteQuery(c)

	assert.Equal(t, http.StatusBadRequest, w.Code)

	var resp []interface{}
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
	assert.NotEmpty(t, resp)

	// Verify the error contains the expected error code
	errObj := resp[0].(map[string]interface{})
	assert.Equal(t, "MALFORMED_QUERY", errObj["errorCode"])
}

func TestQueryHandler_ExecuteQuery_InvalidSOQL(t *testing.T) {
	gin.SetMode(gin.TestMode)

	mockQuery := new(MockQueryService)
	handler := newQueryHandler(mockQuery)

	w, c := newQueryTestContext("GET", "/query?q="+url.QueryEscape("NOT VALID SOQL"))

	handler.ExecuteQuery(c)

	assert.Equal(t, http.StatusBadRequest, w.Code)

	var resp []interface{}
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
	assert.NotEmpty(t, resp)

	errObj := resp[0].(map[string]interface{})
	assert.Equal(t, "MALFORMED_QUERY", errObj["errorCode"])
}

func TestQueryHandler_ExecuteQuery_NoUser(t *testing.T) {
	gin.SetMode(gin.TestMode)

	mockQuery := new(MockQueryService)
	handler := newQueryHandler(mockQuery)

	// Create context WITHOUT setting the user
	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	soql := "SELECT Id FROM Account"
	c.Request = httptest.NewRequest("GET", "/query?q="+url.QueryEscape(soql), nil)

	assert.Panics(t, func() { handler.ExecuteQuery(c) })
}

func TestQueryHandler_ExecuteQuery_ServiceError(t *testing.T) {
	gin.SetMode(gin.TestMode)

	mockQuery := new(MockQueryService)
	handler := newQueryHandler(mockQuery)

	soql := "SELECT Id FROM Account"
	w, c := newQueryTestContext("GET", "/query?q="+url.QueryEscape(soql))

	mockQuery.On("Query", mock.Anything, mock.Anything, mock.Anything).
		Return(nil, fmt.Errorf("table not found"))

	handler.ExecuteQuery(c)

	assert.Equal(t, http.StatusBadRequest, w.Code)

	var resp []interface{}
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
	errObj := resp[0].(map[string]interface{})
	assert.Equal(t, "INVALID_QUERY", errObj["errorCode"])
	assert.Contains(t, errObj["message"], "table not found")

	mockQuery.AssertExpectations(t)
}

func TestQueryHandler_ExecuteQuery_Pagination(t *testing.T) {
	gin.SetMode(gin.TestMode)

	mockQuery := new(MockQueryService)
	handler := newQueryHandler(mockQuery)

	soql := "SELECT Id FROM Account"
	w, c := newQueryTestContext("GET", "/query?q="+url.QueryEscape(soql))

	mockQuery.On("Query", mock.Anything, mock.Anything, mock.Anything).
		Return(&models.QueryResult{
			TotalSize: 1000,
			Done:      false,
			Records: []models.SObject{
				{"Id": "001A"},
			},
		}, nil)

	handler.ExecuteQuery(c)

	assert.Equal(t, http.StatusOK, w.Code)

	var resp map[string]interface{}
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))

	assert.Equal(t, false, resp["done"])
	assert.NotEmpty(t, resp["nextRecordsUrl"])

	// Verify the nextRecordsUrl contains a valid base64 locator
	nextURL := resp["nextRecordsUrl"].(string)
	assert.Contains(t, nextURL, "/services/data/")
	assert.Contains(t, nextURL, "/query/")

	mockQuery.AssertExpectations(t)
}

// ---------------------------------------------------------------------------
// ExecuteQueryAll — GET /queryAll?q=...
// ---------------------------------------------------------------------------

func TestQueryHandler_ExecuteQueryAll_SetsScanAll(t *testing.T) {
	gin.SetMode(gin.TestMode)

	mockQuery := new(MockQueryService)
	handler := newQueryHandler(mockQuery)

	soql := "SELECT Id FROM Account"
	w, c := newQueryTestContext("GET", "/queryAll?q="+url.QueryEscape(soql))

	mockQuery.On("Query", mock.Anything, mock.MatchedBy(func(req interface{}) bool {
		// We cannot easily assert ScanAll in the api.QueryRequest here
		// because it's constructed inside the handler, but we verify it
		// through integration behavior.
		return true
	}), mock.Anything).Return(&models.QueryResult{
		TotalSize: 1,
		Done:      true,
		Records:   []models.SObject{{"Id": "001A"}},
	}, nil)

	handler.ExecuteQueryAll(c)

	assert.Equal(t, http.StatusOK, w.Code)
	mockQuery.AssertExpectations(t)
}

// ---------------------------------------------------------------------------
// QueryMore — GET /query/:locator
// ---------------------------------------------------------------------------

func TestQueryHandler_QueryMore_Success(t *testing.T) {
	gin.SetMode(gin.TestMode)

	mockQuery := new(MockQueryService)
	handler := newQueryHandler(mockQuery)

	// Build a valid locator
	soql := "SELECT Id, Name FROM Account"
	locatorJSON := fmt.Sprintf(`{"q":"%s","offset":500,"limit":500,"scanAll":false}`,
		url.QueryEscape(soql))
	locator := base64.URLEncoding.EncodeToString([]byte(locatorJSON))

	w, c := newQueryTestContext("GET", "/query/"+locator)
	c.Params = gin.Params{{Key: "locator", Value: locator}}

	mockQuery.On("Query", mock.Anything, mock.Anything, mock.Anything).
		Return(&models.QueryResult{
			TotalSize: 1000,
			Done:      true,
			Records: []models.SObject{
				{"Id": "001C", "Name": "Third Corp"},
			},
		}, nil)

	handler.QueryMore(c)

	assert.Equal(t, http.StatusOK, w.Code)

	var resp map[string]interface{}
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
	assert.Equal(t, true, resp["done"])
	assert.Equal(t, float64(1000), resp["totalSize"])

	records := resp["records"].([]interface{})
	assert.Len(t, records, 1)

	mockQuery.AssertExpectations(t)
}

func TestQueryHandler_QueryMore_EmptyLocator(t *testing.T) {
	gin.SetMode(gin.TestMode)

	mockQuery := new(MockQueryService)
	handler := newQueryHandler(mockQuery)

	w, c := newQueryTestContext("GET", "/query/")
	c.Params = gin.Params{{Key: "locator", Value: ""}}

	handler.QueryMore(c)

	assert.Equal(t, http.StatusBadRequest, w.Code)

	var resp []interface{}
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
	errObj := resp[0].(map[string]interface{})
	assert.Equal(t, "INVALID_QUERY_LOCATOR", errObj["errorCode"])
}

func TestQueryHandler_QueryMore_InvalidBase64(t *testing.T) {
	gin.SetMode(gin.TestMode)

	mockQuery := new(MockQueryService)
	handler := newQueryHandler(mockQuery)

	w, c := newQueryTestContext("GET", "/query/not-valid-base64!!!")
	c.Params = gin.Params{{Key: "locator", Value: "not-valid-base64!!!"}}

	handler.QueryMore(c)

	assert.Equal(t, http.StatusBadRequest, w.Code)

	var resp []interface{}
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
	errObj := resp[0].(map[string]interface{})
	assert.Equal(t, "INVALID_QUERY_LOCATOR", errObj["errorCode"])
}

func TestQueryHandler_QueryMore_MalformedLocatorJSON(t *testing.T) {
	gin.SetMode(gin.TestMode)

	mockQuery := new(MockQueryService)
	handler := newQueryHandler(mockQuery)

	// Valid base64 but invalid JSON inside
	locator := base64.URLEncoding.EncodeToString([]byte("not json"))

	w, c := newQueryTestContext("GET", "/query/"+locator)
	c.Params = gin.Params{{Key: "locator", Value: locator}}

	handler.QueryMore(c)

	assert.Equal(t, http.StatusBadRequest, w.Code)

	var resp []interface{}
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
	errObj := resp[0].(map[string]interface{})
	assert.Equal(t, "INVALID_QUERY_LOCATOR", errObj["errorCode"])
}

func TestQueryHandler_QueryMore_NoUser(t *testing.T) {
	gin.SetMode(gin.TestMode)

	mockQuery := new(MockQueryService)
	handler := newQueryHandler(mockQuery)

	soql := "SELECT Id FROM Account"
	locatorJSON := fmt.Sprintf(`{"q":"%s","offset":0,"limit":500,"scanAll":false}`,
		url.QueryEscape(soql))
	locator := base64.URLEncoding.EncodeToString([]byte(locatorJSON))

	// No user in context
	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Request = httptest.NewRequest("GET", "/query/"+locator, nil)
	c.Params = gin.Params{{Key: "locator", Value: locator}}

	assert.Panics(t, func() { handler.QueryMore(c) })
}

func TestQueryHandler_QueryMore_ContinuedPagination(t *testing.T) {
	gin.SetMode(gin.TestMode)

	mockQuery := new(MockQueryService)
	handler := newQueryHandler(mockQuery)

	soql := "SELECT Id FROM Account"
	locatorJSON := fmt.Sprintf(`{"q":"%s","offset":500,"limit":500,"scanAll":false}`,
		url.QueryEscape(soql))
	locator := base64.URLEncoding.EncodeToString([]byte(locatorJSON))

	w, c := newQueryTestContext("GET", "/query/"+locator)
	c.Params = gin.Params{{Key: "locator", Value: locator}}

	// Return a result that is NOT done — should produce another nextRecordsUrl
	mockQuery.On("Query", mock.Anything, mock.Anything, mock.Anything).
		Return(&models.QueryResult{
			TotalSize: 2000,
			Done:      false,
			Records: []models.SObject{
				{"Id": "001D"},
			},
		}, nil)

	handler.QueryMore(c)

	assert.Equal(t, http.StatusOK, w.Code)

	var resp map[string]interface{}
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))

	assert.Equal(t, false, resp["done"])
	assert.NotEmpty(t, resp["nextRecordsUrl"])

	// Decode the next locator and verify offset was incremented
	nextURL := resp["nextRecordsUrl"].(string)
	// Extract locator from URL: /services/data/v63.0/query/<locator>
	parts := splitLastPathSegment(nextURL)
	nextLocatorBytes, err := base64.URLEncoding.DecodeString(parts)
	require.NoError(t, err)

	var nextLocData struct {
		Offset int `json:"offset"`
		Limit  int `json:"limit"`
	}
	require.NoError(t, json.Unmarshal(nextLocatorBytes, &nextLocData))
	assert.Equal(t, 1000, nextLocData.Offset, "Offset should advance by limit")
	assert.Equal(t, 500, nextLocData.Limit)

	mockQuery.AssertExpectations(t)
}

// splitLastPathSegment extracts the last path segment from a URL path.
func splitLastPathSegment(urlStr string) string {
	parsed, _ := url.Parse(urlStr)
	path := parsed.Path
	for i := len(path) - 1; i >= 0; i-- {
		if path[i] == '/' {
			return path[i+1:]
		}
	}
	return path
}

// ---------------------------------------------------------------------------
// ExecuteQuery with limit/offset query parameters
// ---------------------------------------------------------------------------

func TestQueryHandler_ExecuteQuery_OverrideLimitOffset(t *testing.T) {
	gin.SetMode(gin.TestMode)

	mockQuery := new(MockQueryService)
	handler := newQueryHandler(mockQuery)

	soql := "SELECT Id FROM Account"
	w, c := newQueryTestContext("GET", "/query?q="+url.QueryEscape(soql)+"&limit=10&offset=20")

	mockQuery.On("Query", mock.Anything, mock.MatchedBy(func(req interface{}) bool {
		return true
	}), mock.Anything).Return(&models.QueryResult{
		TotalSize: 1,
		Done:      true,
		Records:   []models.SObject{{"Id": "001A"}},
	}, nil)

	handler.ExecuteQuery(c)

	assert.Equal(t, http.StatusOK, w.Code)
	mockQuery.AssertExpectations(t)
}
