package analytics

import (
	"context"
	"fmt"
	"testing"

	"github.com/nexuscrm/shared/pkg/api"
	"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"
)

// --- validateFilters tests ---

func TestValidateFilters(t *testing.T) {
	svc := &DashboardService{}

	t.Run("nil filters passes", func(t *testing.T) {
		err := svc.validateFilters(nil)
		assert.NoError(t, err)
	})

	t.Run("empty filters passes", func(t *testing.T) {
		err := svc.validateFilters(map[string]interface{}{})
		assert.NoError(t, err)
	})

	t.Run("allowed keys pass", func(t *testing.T) {
		filters := map[string]interface{}{
			"startDate": "2025-01-01",
			"endDate":   "2025-12-31",
			"record":    "001xxx",
			"ownerId":   "user-1",
			"name":      "Test",
		}
		err := svc.validateFilters(filters)
		assert.NoError(t, err)
	})

	t.Run("disallowed key fails", func(t *testing.T) {
		filters := map[string]interface{}{
			"evilKey": "drop table",
		}
		err := svc.validateFilters(filters)
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "disallowed filter key: evilKey")
	})

	t.Run("mixed allowed and disallowed fails", func(t *testing.T) {
		filters := map[string]interface{}{
			"name":      "OK",
			"badFilter": "not ok",
		}
		err := svc.validateFilters(filters)
		assert.Error(t, err)
	})
}

// --- DashboardService.GetDashboard tests ---

func TestDashboardService_GetDashboard(t *testing.T) {
	user := &models.UserSession{ID: "user-1", ProfileID: constants.ProfileSystemAdmin}

	t.Run("found by developer name", func(t *testing.T) {
		mqs := new(mockQueryService)
		rptSvc := NewReportService(mqs, nil, nil, nil)
		svc := NewDashboardService(mqs, rptSvc, nil)

		dashRow := models.SObject{
			"Id":            "dash-1",
			"Name":          "Sales Dashboard",
			"DeveloperName": "sales_dashboard",
			"Description":   "Sales metrics",
			"LayoutType":    "TwoColumn",
		}

		// Query by DeveloperName succeeds
		mqs.On("Query", mock.Anything, mock.MatchedBy(func(req api.QueryRequest) bool {
			return req.ObjectAPIName == constants.TableDashboard &&
				len(req.Criteria) == 1 && req.Criteria[0].Field == "DeveloperName"
		}), user).Return(&models.QueryResult{Records: []models.SObject{dashRow}}, nil)

		// Query components
		compRow := models.SObject{
			"Id":       "comp-1",
			"ReportId": "rpt-1",
			"Type":     "Chart",
			"Title":    "Revenue Chart",
		}
		mqs.On("Query", mock.Anything, mock.MatchedBy(func(req api.QueryRequest) bool {
			return req.ObjectAPIName == constants.TableDashboardComponent
		}), user).Return(&models.QueryResult{Records: []models.SObject{compRow}}, nil)

		def, err := svc.GetDashboard(context.Background(), "sales_dashboard", user)
		require.NoError(t, err)
		assert.Equal(t, "dash-1", def.ID)
		assert.Equal(t, "Sales Dashboard", def.Name)
		assert.Equal(t, "sales_dashboard", def.DeveloperName)
		assert.Equal(t, "TwoColumn", def.LayoutType)
		require.Len(t, def.Components, 1)
		assert.Equal(t, "comp-1", def.Components[0].ID)
		assert.Equal(t, "rpt-1", def.Components[0].ReportID)
	})

	t.Run("found by ID when developer name not found", func(t *testing.T) {
		mqs := new(mockQueryService)
		rptSvc := NewReportService(mqs, nil, nil, nil)
		svc := NewDashboardService(mqs, rptSvc, nil)

		dashRow := models.SObject{
			"Id":            "dash-2",
			"Name":          "Pipeline Dashboard",
			"DeveloperName": "pipeline",
			"Description":   "",
			"LayoutType":    "SingleColumn",
		}

		// Query by DeveloperName returns empty
		mqs.On("Query", mock.Anything, mock.MatchedBy(func(req api.QueryRequest) bool {
			return req.ObjectAPIName == constants.TableDashboard &&
				len(req.Criteria) == 1 && req.Criteria[0].Field == "DeveloperName"
		}), user).Return(&models.QueryResult{Records: []models.SObject{}}, nil)

		// Query by Id succeeds
		mqs.On("Query", mock.Anything, mock.MatchedBy(func(req api.QueryRequest) bool {
			return req.ObjectAPIName == constants.TableDashboard &&
				len(req.Criteria) == 1 && req.Criteria[0].Field == "Id"
		}), user).Return(&models.QueryResult{Records: []models.SObject{dashRow}}, nil)

		// No components
		mqs.On("Query", mock.Anything, mock.MatchedBy(func(req api.QueryRequest) bool {
			return req.ObjectAPIName == constants.TableDashboardComponent
		}), user).Return(&models.QueryResult{Records: []models.SObject{}}, nil)

		def, err := svc.GetDashboard(context.Background(), "dash-2", user)
		require.NoError(t, err)
		assert.Equal(t, "dash-2", def.ID)
		assert.Empty(t, def.Components)
	})

	t.Run("not found returns error", func(t *testing.T) {
		mqs := new(mockQueryService)
		rptSvc := NewReportService(mqs, nil, nil, nil)
		svc := NewDashboardService(mqs, rptSvc, nil)

		// Both lookups return empty
		mqs.On("Query", mock.Anything, mock.MatchedBy(func(req api.QueryRequest) bool {
			return req.ObjectAPIName == constants.TableDashboard
		}), user).Return(&models.QueryResult{Records: []models.SObject{}}, nil)

		_, err := svc.GetDashboard(context.Background(), "nonexistent", user)
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "not found")
	})

	t.Run("query error on first lookup passes through to second", func(t *testing.T) {
		mqs := new(mockQueryService)
		rptSvc := NewReportService(mqs, nil, nil, nil)
		svc := NewDashboardService(mqs, rptSvc, nil)

		// First query (by DeveloperName) fails
		mqs.On("Query", mock.Anything, mock.MatchedBy(func(req api.QueryRequest) bool {
			return req.ObjectAPIName == constants.TableDashboard &&
				len(req.Criteria) == 1 && req.Criteria[0].Field == "DeveloperName"
		}), user).Return(nil, fmt.Errorf("dev name lookup error"))

		// Second query (by Id) also fails
		mqs.On("Query", mock.Anything, mock.MatchedBy(func(req api.QueryRequest) bool {
			return req.ObjectAPIName == constants.TableDashboard &&
				len(req.Criteria) == 1 && req.Criteria[0].Field == "Id"
		}), user).Return(nil, fmt.Errorf("id lookup error"))

		_, err := svc.GetDashboard(context.Background(), "anything", user)
		assert.Error(t, err)
	})

	t.Run("component with JSON config is parsed", func(t *testing.T) {
		mqs := new(mockQueryService)
		rptSvc := NewReportService(mqs, nil, nil, nil)
		svc := NewDashboardService(mqs, rptSvc, nil)

		dashRow := models.SObject{
			"Id":            "dash-3",
			"Name":          "Config Dashboard",
			"DeveloperName": "config_dash",
		}
		mqs.On("Query", mock.Anything, mock.MatchedBy(func(req api.QueryRequest) bool {
			return req.ObjectAPIName == constants.TableDashboard &&
				req.Criteria[0].Field == "DeveloperName"
		}), user).Return(&models.QueryResult{Records: []models.SObject{dashRow}}, nil)

		compRow := models.SObject{
			"Id":       "comp-2",
			"ReportId": "rpt-2",
			"Type":     "Metric",
			"Title":    "Total Revenue",
			"Config":   `{"x":0,"y":0,"w":6,"h":4}`,
		}
		mqs.On("Query", mock.Anything, mock.MatchedBy(func(req api.QueryRequest) bool {
			return req.ObjectAPIName == constants.TableDashboardComponent
		}), user).Return(&models.QueryResult{Records: []models.SObject{compRow}}, nil)

		def, err := svc.GetDashboard(context.Background(), "config_dash", user)
		require.NoError(t, err)
		require.Len(t, def.Components, 1)
		assert.NotNil(t, def.Components[0].Config)
		assert.Equal(t, float64(6), def.Components[0].Config["w"])
	})

	t.Run("component with invalid JSON config is ignored gracefully", func(t *testing.T) {
		mqs := new(mockQueryService)
		rptSvc := NewReportService(mqs, nil, nil, nil)
		svc := NewDashboardService(mqs, rptSvc, nil)

		dashRow := models.SObject{
			"Id":            "dash-4",
			"Name":          "Bad Config Dashboard",
			"DeveloperName": "bad_config",
		}
		mqs.On("Query", mock.Anything, mock.MatchedBy(func(req api.QueryRequest) bool {
			return req.ObjectAPIName == constants.TableDashboard &&
				req.Criteria[0].Field == "DeveloperName"
		}), user).Return(&models.QueryResult{Records: []models.SObject{dashRow}}, nil)

		compRow := models.SObject{
			"Id":       "comp-3",
			"ReportId": "rpt-3",
			"Type":     "Chart",
			"Title":    "Bad Config",
			"Config":   `{invalid json}`,
		}
		mqs.On("Query", mock.Anything, mock.MatchedBy(func(req api.QueryRequest) bool {
			return req.ObjectAPIName == constants.TableDashboardComponent
		}), user).Return(&models.QueryResult{Records: []models.SObject{compRow}}, nil)

		def, err := svc.GetDashboard(context.Background(), "bad_config", user)
		require.NoError(t, err)
		require.Len(t, def.Components, 1)
		// Config should be nil because JSON parse failed
		assert.Nil(t, def.Components[0].Config)
	})

	t.Run("component fetch error returns error", func(t *testing.T) {
		mqs := new(mockQueryService)
		rptSvc := NewReportService(mqs, nil, nil, nil)
		svc := NewDashboardService(mqs, rptSvc, nil)

		dashRow := models.SObject{"Id": "dash-5", "Name": "Test"}
		mqs.On("Query", mock.Anything, mock.MatchedBy(func(req api.QueryRequest) bool {
			return req.ObjectAPIName == constants.TableDashboard &&
				req.Criteria[0].Field == "DeveloperName"
		}), user).Return(&models.QueryResult{Records: []models.SObject{dashRow}}, nil)

		mqs.On("Query", mock.Anything, mock.MatchedBy(func(req api.QueryRequest) bool {
			return req.ObjectAPIName == constants.TableDashboardComponent
		}), user).Return(nil, fmt.Errorf("component fetch error"))

		_, err := svc.GetDashboard(context.Background(), "dash-5", user)
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "failed to fetch components")
	})
}

// --- DashboardService.RunDashboard tests ---

func TestDashboardService_RunDashboard(t *testing.T) {
	user := &models.UserSession{ID: "user-1", ProfileID: constants.ProfileSystemAdmin}

	t.Run("runs dashboard with no filters", func(t *testing.T) {
		mqs := new(mockQueryService)
		rptSvc := NewReportService(mqs, nil, nil, nil)
		svc := NewDashboardService(mqs, rptSvc, nil)

		// Dashboard lookup
		dashRow := models.SObject{
			"Id":            "dash-1",
			"Name":          "Test Dashboard",
			"DeveloperName": "test_dash",
		}
		mqs.On("Query", mock.Anything, mock.MatchedBy(func(req api.QueryRequest) bool {
			return req.ObjectAPIName == constants.TableDashboard &&
				len(req.Criteria) == 1 && req.Criteria[0].Field == "DeveloperName"
		}), user).Return(&models.QueryResult{Records: []models.SObject{dashRow}}, nil)

		// No components
		mqs.On("Query", mock.Anything, mock.MatchedBy(func(req api.QueryRequest) bool {
			return req.ObjectAPIName == constants.TableDashboardComponent
		}), user).Return(&models.QueryResult{Records: []models.SObject{}}, nil)

		result, err := svc.RunDashboard(context.Background(), "test_dash", user, nil)
		require.NoError(t, err)
		assert.Equal(t, "dash-1", result.DashboardMetadata.ID)
		assert.NotNil(t, result.ComponentData)
		assert.Empty(t, result.ComponentData)
	})

	t.Run("rejects disallowed filter key", func(t *testing.T) {
		mqs := new(mockQueryService)
		rptSvc := NewReportService(mqs, nil, nil, nil)
		svc := NewDashboardService(mqs, rptSvc, nil)

		// Dashboard lookup
		dashRow := models.SObject{
			"Id":            "dash-1",
			"Name":          "Test Dashboard",
			"DeveloperName": "test_dash",
		}
		mqs.On("Query", mock.Anything, mock.MatchedBy(func(req api.QueryRequest) bool {
			return req.ObjectAPIName == constants.TableDashboard &&
				len(req.Criteria) == 1 && req.Criteria[0].Field == "DeveloperName"
		}), user).Return(&models.QueryResult{Records: []models.SObject{dashRow}}, nil)

		mqs.On("Query", mock.Anything, mock.MatchedBy(func(req api.QueryRequest) bool {
			return req.ObjectAPIName == constants.TableDashboardComponent
		}), user).Return(&models.QueryResult{Records: []models.SObject{}}, nil)

		filters := map[string]interface{}{"sqlInjection": "1=1"}
		_, err := svc.RunDashboard(context.Background(), "test_dash", user, filters)
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "disallowed filter key")
	})

	t.Run("dashboard not found propagates error", func(t *testing.T) {
		mqs := new(mockQueryService)
		rptSvc := NewReportService(mqs, nil, nil, nil)
		svc := NewDashboardService(mqs, rptSvc, nil)

		mqs.On("Query", mock.Anything, mock.MatchedBy(func(req api.QueryRequest) bool {
			return req.ObjectAPIName == constants.TableDashboard
		}), user).Return(&models.QueryResult{Records: []models.SObject{}}, nil)

		_, err := svc.RunDashboard(context.Background(), "nonexistent", user, nil)
		assert.Error(t, err)
	})

	t.Run("component report execution error propagates", func(t *testing.T) {
		mqs := new(mockQueryService)
		rptSvc := NewReportService(mqs, nil, nil, nil)
		svc := NewDashboardService(mqs, rptSvc, nil)

		dashRow := models.SObject{"Id": "dash-1", "Name": "Test", "DeveloperName": "test_dash"}
		mqs.On("Query", mock.Anything, mock.MatchedBy(func(req api.QueryRequest) bool {
			return req.ObjectAPIName == constants.TableDashboard && req.Criteria[0].Field == "DeveloperName"
		}), user).Return(&models.QueryResult{Records: []models.SObject{dashRow}}, nil)

		compRow := models.SObject{"Id": "comp-1", "ReportId": "rpt-1", "Type": "Chart", "Title": "Test"}
		mqs.On("Query", mock.Anything, mock.MatchedBy(func(req api.QueryRequest) bool {
			return req.ObjectAPIName == constants.TableDashboardComponent
		}), user).Return(&models.QueryResult{Records: []models.SObject{compRow}}, nil)

		// Report definition fetch fails
		mqs.On("Query", mock.Anything, mock.MatchedBy(func(req api.QueryRequest) bool {
			return req.ObjectAPIName == constants.TableReport
		}), user).Return(nil, fmt.Errorf("report fetch error"))

		_, err := svc.RunDashboard(context.Background(), "test_dash", user, nil)
		assert.Error(t, err)
	})
}
