package sfcrmappservice

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"sort"
	"strings"
	"testing"

	catalogsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/catalog/consumerv1"
	permissionsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/effectivepermissionsv1"
	querysemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/queryv1"
	reportsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/reportv1"
)

func TestTemporaryAccountHistoryReportUsesAuthorizedRuntimeHistory(t *testing.T) {
	config := temporaryHistoryReportConfig(t, []searchObjectSpec{{
		APIName: "Account", Label: "Account",
		Fields: []searchFieldSpec{
			{APIName: "Id", Label: "Account ID", Type: "string"},
			{APIName: "OwnerId", Label: "Owner", Type: "reference"},
			{APIName: "Name", Label: "Account Name", Type: "string"},
		},
	}}, nil)
	config.RuntimeQuery = func(_ context.Context, bearer string, request querysemantic.Request) (querysemantic.Response, error) {
		if bearer != "user-secret" || request.ObjectID != "Account" || request.Limit != 1 {
			t.Fatalf("query bearer=%q request=%#v", bearer, request)
		}
		return analyticsQueryResponse(analyticsQueryRecord("Account",
			"Id", "001", "Name", "Updated Account", "OwnerId", "005",
		)), nil
	}
	oldValue, newValue := "Original Account", "Updated Account"
	config.RuntimeHistoryQuery = func(_ context.Context, bearer string, request querysemantic.HistoryRequest) (querysemantic.HistoryResponse, error) {
		if bearer != "user-secret" || request.ParentObjectID != "Account" || request.ParentRecordID != "001" ||
			request.MinCatalogRevision == nil || *request.MinCatalogRevision != 17 || !request.SortDescending {
			t.Fatalf("history bearer=%q request=%#v", bearer, request)
		}
		return querysemantic.HistoryResponse{
			TotalSize: 1, Done: true, Receipts: analyticsQueryReceipts(),
			Rows: []querysemantic.HistoryRow{{
				ID: "017", ParentID: "001", Field: "Name", OldValue: &oldValue, NewValue: &newValue,
				DataType: "string", CreatedDate: "2026-08-14T12:00:00Z", CreatedByID: "005",
			}},
		}, nil
	}
	service, err := NewService(config)
	if err != nil {
		t.Fatal(err)
	}
	body := `{"reportMetadata":{"name":"Account History","reportType":{"type":"AccountHistory"},"reportFormat":"TABULAR","detailColumns":["AccountId","AccountName","Field","OldValue","NewValue","CreatedDate"],"reportFilters":[{"column":"AccountId","operator":"equals","value":"001"},{"column":"Field","operator":"equals","value":"Name"}]}}`
	response := executeTemporaryReport(t, service, body)
	if response.ReportMetadata.ReportType.Type != "AccountHistory" || !response.HasDetailRows ||
		len(response.FactMap["T!T"].Rows) != 1 {
		t.Fatalf("response = %#v", response)
	}
	cells := response.FactMap["T!T"].Rows[0].DataCells
	if cells[2].Value != "Name" || cells[3].Value != oldValue || cells[4].Value != newValue {
		t.Fatalf("cells = %#v", cells)
	}
}

func TestOpportunityFieldHistoryUsesTheGenericFieldHistorySource(t *testing.T) {
	source, err := historyReportSourceFromMetadata(
		"OpportunityFieldHistory", catalogsemantic.AppMetadataViewV3{},
	)
	if err != nil {
		t.Fatal(err)
	}
	if source.ParentObject != "Opportunity" || source.ParentIDColumn != "OpportunityId" ||
		source.ParentNameColumn != "OpportunityName" {
		t.Fatalf("source = %#v", source)
	}
	definition := temporaryReportDefinition{
		Name: "Opportunity Field History", ReportType: temporaryReportType{Type: source.SourceRef},
		ReportFormat:  "TABULAR",
		DetailColumns: []string{"OpportunityId", "OpportunityName", "Field", "OldValue", "NewValue", "CreatedDate"},
		ReportFilters: []canonicalReportFilter{
			{Column: "OpportunityId", Operator: "equals", Value: "006"},
			{Column: "Field", Operator: "equals", Value: "StageName"},
		},
	}
	if err := normalizeTemporaryReportDefinition(&definition, source); err != nil {
		t.Fatal(err)
	}
	parentID, predicate, err := fieldHistoryReportPredicate(definition.ReportFilters, source.ParentIDColumn)
	if err != nil || parentID != "006" || predicate.Kind != querysemantic.PredicateFieldComparison || predicate.Field != "Field" {
		t.Fatalf("parentID=%q predicate=%#v err=%v", parentID, predicate, err)
	}
}

func TestTemporaryOpportunityHistoryUsesCatalogColumnsAndLatestStageFirst(t *testing.T) {
	objects := []searchObjectSpec{
		{
			APIName: "Opportunity", Label: "Opportunity",
			Fields: []searchFieldSpec{
				{APIName: "Id", Label: "Opportunity ID", Type: "string"},
				{APIName: "Name", Label: "Opportunity Name", Type: "string"},
				{APIName: "OwnerId", Label: "Owner", Type: "reference"},
				{APIName: "StageName", Label: "Stage", Type: "picklist"},
				{APIName: "Probability", Label: "Probability", Type: "percent"},
				{APIName: "CloseDate", Label: "Close Date", Type: "date"},
				{APIName: "ForecastCategory", Label: "Forecast Category", Type: "picklist"},
				{APIName: "Region__c", Label: "Region", Type: "picklist"},
				{APIName: "ARR_Amount_USD__c", Label: "ARR", Type: "currency"},
				{APIName: "ACV_Amount_do_not_use__c", Label: "ACV", Type: "currency"},
			},
		},
		{APIName: "User", Label: "User", Fields: []searchFieldSpec{
			{APIName: "Id", Label: "User ID", Type: "string"},
			{APIName: "Name", Label: "Name", Type: "string"},
			{APIName: "UserRoleId", Label: "Role", Type: "reference"},
		}},
		{APIName: "UserRole", Label: "User Role", Fields: []searchFieldSpec{
			{APIName: "Id", Label: "Role ID", Type: "string"},
			{APIName: "Name", Label: "Name", Type: "string"},
		}},
	}
	reportTypes := map[string]any{
		"fullName": "OpportunityHistory", "label": "Opportunity History",
		"baseObject": "Opportunity", "deployed": true, "autogenerated": false,
		"metadata": map[string]any{"virtualSource": map[string]any{
			"sourceRef": "OpportunityHistory",
			"configuration": map[string]any{"opportunityHistoryColumns": []map[string]any{
				{"name": "Region", "fieldAPIName": "Region__c"},
				{"name": "ARRAmountUSD", "fieldAPIName": "ARR_Amount_USD__c", "historicalSnapshot": true},
				{"name": "ACVAmountDoNotUse", "fieldAPIName": "ACV_Amount_do_not_use__c", "historicalSnapshot": true},
			}},
		}},
	}
	config := temporaryHistoryReportConfig(t, objects, reportTypes)
	config.RuntimeQuery = func(_ context.Context, _ string, request querysemantic.Request) (querysemantic.Response, error) {
		switch request.ObjectID {
		case "Opportunity":
			return analyticsQueryResponse(analyticsQueryRecord("Opportunity",
				"Id", "006", "Name", "History Probe", "OwnerId", "005", "StageName", "Technical Validation",
				"Probability", "30", "CloseDate", "2026-09-30", "ForecastCategory", "BestCase",
				"Region__c", "EMEA", "ARR_Amount_USD__c", "70000", "ACV_Amount_do_not_use__c", "20000",
			)), nil
		case "User":
			return analyticsQueryResponse(analyticsQueryRecord("User", "Id", "005", "Name", "Owner", "UserRoleId", "00E")), nil
		case "UserRole":
			return analyticsQueryResponse(analyticsQueryRecord("UserRole", "Id", "00E", "Name", "Sales")), nil
		default:
			t.Fatalf("unexpected query request %#v", request)
			return querysemantic.Response{}, nil
		}
	}
	oldProspecting, discovery := "Prospecting", "Discovery & Qualification"
	technical := "Technical Validation"
	p10, p20, p30 := "10", "20", "30"
	a50, a60, a70 := "50000", "60000", "70000"
	c0, c10, c20 := "0", "10000", "20000"
	config.RuntimeHistoryQuery = func(_ context.Context, _ string, request querysemantic.HistoryRequest) (querysemantic.HistoryResponse, error) {
		if request.ParentObjectID != "Opportunity" || request.ParentRecordID != "006" {
			t.Fatalf("history request = %#v", request)
		}
		rows := []querysemantic.HistoryRow{
			historyTestRow("h1-stage", "006", "StageName", &oldProspecting, &discovery, "2026-08-14T12:00:00Z"),
			historyTestRow("h1-prob", "006", "Probability", &p10, &p20, "2026-08-14T12:00:00Z"),
			historyTestRow("h1-arr", "006", "ARR_Amount_USD__c", &a50, &a60, "2026-08-14T12:00:00Z"),
			historyTestRow("h1-acv", "006", "ACV_Amount_do_not_use__c", &c0, &c10, "2026-08-14T12:00:00Z"),
			historyTestRow("h2-stage", "006", "StageName", &discovery, &technical, "2026-08-14T12:01:30Z"),
			historyTestRow("h2-prob", "006", "Probability", &p20, &p30, "2026-08-14T12:01:30Z"),
			historyTestRow("h2-arr", "006", "ARR_Amount_USD__c", &a60, &a70, "2026-08-14T12:01:30Z"),
			historyTestRow("h2-acv", "006", "ACV_Amount_do_not_use__c", &c10, &c20, "2026-08-14T12:01:30Z"),
		}
		return querysemantic.HistoryResponse{TotalSize: uint64(len(rows)), Done: true, Rows: rows, Receipts: analyticsQueryReceipts()}, nil
	}
	service, err := NewService(config)
	if err != nil {
		t.Fatal(err)
	}
	body := `{"reportMetadata":{"name":"Opportunity History","reportType":{"type":"OpportunityHistory"},"reportFormat":"SUMMARY","detailColumns":["OpportunityId","OpportunityName","Region","OwnerRole","FromStageName","StageName","StageDuration","Probability","CloseDate","ForecastCategory","ARRAmountUSD","ACVAmountDoNotUse","CreatedDate"],"reportFilters":[{"column":"OpportunityId","operator":"equals","value":"006"}],"groupingsDown":[{"name":"Region","dateGranularity":"None"},{"name":"OwnerRole","dateGranularity":"None"},{"name":"StageName","dateGranularity":"None"}]}}`
	response := executeTemporaryReport(t, service, body)
	groups := response.GroupingsDown.Groupings
	if len(groups) != 1 || groups[0].Value != "EMEA" || len(groups[0].Groupings) != 1 ||
		groups[0].Groupings[0].Value != "Sales" || len(groups[0].Groupings[0].Groupings) != 2 ||
		groups[0].Groupings[0].Groupings[0].Value != technical {
		t.Fatalf("groupings = %#v", groups)
	}
	latest := response.FactMap["0_0_0!T"].Rows
	if len(latest) != 1 {
		t.Fatalf("latest fact = %#v", response.FactMap["0_0_0!T"])
	}
	cells := latest[0].DataCells
	if cells[4].Value != discovery || cells[5].Value != technical || cells[7].Value != "30" ||
		cells[8].Value != "2026-09-30" || cells[9].Value != "BestCase" || cells[10].Value != "70000" || cells[11].Value != "20000" {
		t.Fatalf("latest cells = %#v", cells)
	}
}

func TestTemporaryReportDefinitionRejectsUnsupportedSourceBeforeRuntimeReads(t *testing.T) {
	config := temporaryHistoryReportConfig(t, []searchObjectSpec{{APIName: "Account", Label: "Account", Fields: []searchFieldSpec{{APIName: "Id", Label: "ID", Type: "string"}}}}, nil)
	queryCalls := 0
	config.RuntimeQuery = func(context.Context, string, querysemantic.Request) (querysemantic.Response, error) {
		queryCalls++
		return querysemantic.Response{}, nil
	}
	service, err := NewService(config)
	if err != nil {
		t.Fatal(err)
	}
	request := httptest.NewRequest(http.MethodPost, "/services/data/v63.0/analytics/reports/execute", strings.NewReader(
		`{"reportMetadata":{"name":"Bad","reportType":{"type":"TenantThing__c"},"reportFormat":"TABULAR","detailColumns":["Id"]}}`,
	))
	request.Header.Set("Authorization", "Bearer user-secret")
	recorder := httptest.NewRecorder()
	service.ServeHTTP(recorder, request)
	if recorder.Code != http.StatusBadRequest || queryCalls != 0 {
		t.Fatalf("status=%d queryCalls=%d body=%s", recorder.Code, queryCalls, recorder.Body.String())
	}
}

func temporaryHistoryReportConfig(t *testing.T, objects []searchObjectSpec, reportTypes any) ServiceConfig {
	t.Helper()
	permissions := validPermissionResponse(17, 4)
	permissions.SystemPermissions = analyticsTestSystemPermissions()
	config := permissionMetadataServiceConfig(
		func(context.Context, string, permissionsemantic.Request) (permissionsemantic.Response, error) {
			return permissions, nil
		},
		func(context.Context, catalogsemantic.GetRequest) (catalogsemantic.AppMetadataView, error) {
			return catalogsemantic.AppMetadataView{}, nil
		},
	)
	config.RuntimeReadReport = func(context.Context, string, reportsemantic.Request) (reportsemantic.Response, error) {
		return reportsemantic.Response{}, nil
	}
	view := searchTestView(t, objects)
	view.Receipts.SecurityEpoch = 4
	if reportTypes != nil {
		behavior, err := json.Marshal(reportTypes)
		if err != nil {
			t.Fatal(err)
		}
		component := catalogsemantic.EffectiveMetadataComponent{
			ComponentKind:          catalogsemantic.EffectiveReportTypeComponentKind,
			ComponentFormatVersion: 1, BusinessKey: "OpportunityHistory",
			CanonicalBehavior: behavior,
		}
		component.ContentDigest = catalogsemantic.ComputeEffectiveMetadataComponentDigest(behavior)
		view.EffectiveComponents = append(view.EffectiveComponents, component)
		sort.Slice(view.EffectiveComponents, func(left, right int) bool {
			leftKey := view.EffectiveComponents[left].ComponentKind + "\x00" + view.EffectiveComponents[left].BusinessKey
			rightKey := view.EffectiveComponents[right].ComponentKind + "\x00" + view.EffectiveComponents[right].BusinessKey
			return leftKey < rightKey
		})
	}
	view.Lineage.ViewDigest = catalogsemantic.ComputeAppMetadataViewV3Digest(view)
	if err := catalogsemantic.ValidateAppMetadataViewV3(view); err != nil {
		t.Fatalf("temporary report view invalid: %v", err)
	}
	config.RuntimeGetAppMetadataViewV3 = func(_ context.Context, request catalogsemantic.GetRequest) (catalogsemantic.AppMetadataViewV3, error) {
		if request.MinCatalogRevision == nil || *request.MinCatalogRevision != 17 {
			t.Fatalf("metadata request = %#v", request)
		}
		return view, nil
	}
	return config
}

func analyticsQueryReceipts() querysemantic.Receipts {
	return analyticsQueryResponse().Receipts
}

func historyTestRow(id, parentID, field string, oldValue, newValue *string, createdDate string) querysemantic.HistoryRow {
	return querysemantic.HistoryRow{
		ID: id, ParentID: parentID, Field: field, OldValue: oldValue, NewValue: newValue,
		DataType: "string", CreatedDate: createdDate, CreatedByID: "005",
	}
}

type temporaryReportResponse struct {
	ReportMetadata struct {
		ReportType struct {
			Type string `json:"type"`
		} `json:"reportType"`
	} `json:"reportMetadata"`
	HasDetailRows bool `json:"hasDetailRows"`
	FactMap       map[string]struct {
		Rows []struct {
			DataCells []struct {
				Value any `json:"value"`
			} `json:"dataCells"`
		} `json:"rows"`
	} `json:"factMap"`
	GroupingsDown struct {
		Groupings []temporaryGroupingResponse `json:"groupings"`
	} `json:"groupingsDown"`
}

type temporaryGroupingResponse struct {
	Value     string                      `json:"value"`
	Groupings []temporaryGroupingResponse `json:"groupings"`
}

func executeTemporaryReport(t *testing.T, service *Service, body string) temporaryReportResponse {
	t.Helper()
	request := httptest.NewRequest(http.MethodPost, "/services/data/v63.0/analytics/reports/execute", strings.NewReader(body))
	request.Header.Set("Authorization", "Bearer user-secret")
	recorder := httptest.NewRecorder()
	service.ServeHTTP(recorder, request)
	if recorder.Code != http.StatusOK {
		t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
	}
	var response temporaryReportResponse
	if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
		t.Fatal(err)
	}
	return response
}
