package sfcrmappservice

import (
	"context"
	"encoding/json"
	navigationsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/applicationnavigationv1"
	"net/http"
	"net/http/httptest"
	"slices"
	"strings"
	"testing"

	catalogsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/catalog/consumerv1"
	commonsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/commonv1"
	permissionsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/effectivepermissionsv1"
)

const uiAppsTestNavigation = `{"applications":[{"id":"Sales","frontendAppId":"sfcrm","label":"Sales","description":"Sell","icon":"sales","color":"blue","navigationItems":[{"id":"account","type":"object","objectApiName":"Account","label":"Accounts"},{"id":"contact","type":"object","objectApiName":"Contact","label":"Contacts"},{"id":"docs","type":"web","pageUrl":"https://example.test/docs","label":"Docs"}],"utilityItems":[]},{"id":"Service","label":"Service","navigationItems":[],"utilityItems":[]}]}`

func uiAppsTestView(t *testing.T, behavior string) catalogsemantic.AppMetadataViewV3 {
	t.Helper()
	view := searchTestView(t, []searchObjectSpec{{
		APIName: "Account", KeyPrefix: "001", Label: "Account", Fields: []searchFieldSpec{},
	}})
	componentBehavior := []byte(behavior)
	component := catalogsemantic.EffectiveMetadataComponent{
		ComponentKind: applicationNavigationKind, ComponentFormatVersion: 1,
		BusinessKey: applicationNavigationKey, CanonicalBehavior: componentBehavior,
		ContentDigest: catalogsemantic.ComputeEffectiveMetadataComponentDigest(componentBehavior),
	}
	view.EffectiveComponents = append(view.EffectiveComponents, component)
	slices.SortFunc(view.EffectiveComponents, func(left, right catalogsemantic.EffectiveMetadataComponent) int {
		if order := strings.Compare(left.ComponentKind, right.ComponentKind); order != 0 {
			return order
		}
		return strings.Compare(left.BusinessKey, right.BusinessKey)
	})
	view.Lineage.ViewDigest = catalogsemantic.ComputeAppMetadataViewV3Digest(view)
	if err := catalogsemantic.ValidateAppMetadataViewV3(view); err != nil {
		t.Fatalf("UI Apps AppMetadataViewV3 is invalid: %v", err)
	}
	return view
}

func uiAppsTestPermissions(view catalogsemantic.AppMetadataViewV3) permissionsemantic.Response {
	response := validPermissionResponse(view.Receipts.CatalogRevision, view.Receipts.SecurityEpoch)
	response.ApplicationPermissions = []permissionsemantic.ApplicationPermission{
		{ApplicationID: "Sales", CanView: true, IsDefault: true},
		{ApplicationID: "Service", CanView: false},
	}
	response.NavigationPermissions = []permissionsemantic.NavigationPermission{
		{NavigationKey: "Account", CanView: true},
		{NavigationKey: "Contact", CanView: false},
	}
	return response
}

func uiAppsTestService(
	t *testing.T,
	view catalogsemantic.AppMetadataViewV3,
	permissions permissionsemantic.Response,
) *Service {
	t.Helper()
	config := permissionMetadataServiceConfig(
		func(_ context.Context, _ string, request permissionsemantic.Request) (permissionsemantic.Response, error) {
			wantSections := []permissionsemantic.Section{
				permissionsemantic.SectionApplicationPermissions,
				permissionsemantic.SectionNavigationPermissions,
			}
			if request.Selection != nil && request.Selection.Requests(permissionsemantic.SectionSystemPermissions) {
				wantSections = []permissionsemantic.Section{permissionsemantic.SectionSystemPermissions}
			}
			if request.MinCatalogRevision == nil ||
				*request.MinCatalogRevision != view.Receipts.CatalogRevision ||
				request.Selection == nil ||
				!slices.Equal(request.Selection.Sections, wantSections) ||
				len(request.Selection.ResourceTypes) != 0 {
				t.Fatalf("UI Apps permission request=%#v", request)
			}
			return selectedPermissionTestResponse(permissions, request), nil
		},
		func(context.Context, catalogsemantic.GetRequest) (catalogsemantic.AppMetadataView, error) {
			return validSearchScopeView(t, view.Receipts.CatalogRevision, view.Receipts.SecurityEpoch), nil
		},
	)
	config.RuntimeGetAppMetadataViewV3 = func(context.Context, catalogsemantic.GetRequest) (catalogsemantic.AppMetadataViewV3, error) {
		return view, nil
	}
	config.EnableUIApps = true
	config.RuntimeResolveApplicationNavigation = uiAppsTestCurrentNavigation(view, permissions)
	service, err := NewService(config)
	if err != nil {
		t.Fatal(err)
	}
	return service
}

func uiAppsRequest(method, target string) *http.Request {
	request := httptest.NewRequest(method, target, nil)
	request.Header.Set("Authorization", "Bearer user-secret")
	return request
}

func TestUIAppsProjectsEstablishedVisibleNavigationWire(t *testing.T) {
	view := uiAppsTestView(t, uiAppsTestNavigation)
	service := uiAppsTestService(t, view, uiAppsTestPermissions(view))
	recorder := httptest.NewRecorder()
	service.ServeHTTP(recorder, uiAppsRequest(http.MethodGet, "/services/data/v63.0/ui-api/apps?formFactor=Large"))
	if recorder.Code != http.StatusOK {
		t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
	}
	var body struct {
		Apps map[string]uiApp `json:"apps"`
	}
	if err := json.Unmarshal(recorder.Body.Bytes(), &body); err != nil {
		t.Fatal(err)
	}
	sales, ok := body.Apps["Sales"]
	if len(body.Apps) != 1 || !ok {
		t.Fatalf("apps = %#v, want only visible Sales", body.Apps)
	}
	if sales.AppID != "Sales" || sales.FrontendAppID != "sfcrm" ||
		sales.DeveloperName != "Sales" || sales.Label != "Sales" ||
		sales.Description != "Sell" || sales.HeaderColor != "blue" ||
		sales.IconURL != "sales" || !sales.Selected ||
		!slices.Equal(sales.FormFactors, []string{"Large"}) {
		t.Fatalf("Sales projection = %#v", sales)
	}
	if len(sales.NavItems) != 2 ||
		sales.NavItems[0].ObjectAPIName != "Account" ||
		sales.NavItems[1].PageReference == nil ||
		sales.NavItems[1].PageReference.Type != "standard__webPage" ||
		sales.NavItems[1].PageReference.Attributes["url"] != "https://example.test/docs" {
		t.Fatalf("Sales navigation = %#v", sales.NavItems)
	}
}

func TestUIAppsRouteAndActivationRemainClosed(t *testing.T) {
	view := uiAppsTestView(t, uiAppsTestNavigation)
	service := uiAppsTestService(t, view, uiAppsTestPermissions(view))
	for _, version := range []string{"v59.0", "v60.0", "v61.0", "v62.0", "v63.0"} {
		recorder := httptest.NewRecorder()
		service.ServeHTTP(recorder, uiAppsRequest(http.MethodGet, "/services/data/"+version+"/ui-api/apps"))
		if recorder.Code != http.StatusOK {
			t.Fatalf("%s status=%d body=%s", version, recorder.Code, recorder.Body.String())
		}
	}
	unsupported := httptest.NewRecorder()
	service.ServeHTTP(unsupported, uiAppsRequest(http.MethodGet, "/services/data/v58.0/ui-api/apps"))
	if unsupported.Code != http.StatusNotFound {
		t.Fatalf("unsupported version status=%d", unsupported.Code)
	}
	method := httptest.NewRecorder()
	service.ServeHTTP(method, uiAppsRequest(http.MethodPost, "/services/data/v63.0/ui-api/apps"))
	if method.Code != http.StatusMethodNotAllowed || method.Header().Get("Allow") != http.MethodGet {
		t.Fatalf("method status=%d allow=%q", method.Code, method.Header().Get("Allow"))
	}
	unauthenticated := httptest.NewRecorder()
	request := uiAppsRequest(http.MethodGet, "/services/data/v63.0/ui-api/apps")
	request.Header.Del("Authorization")
	service.ServeHTTP(unauthenticated, request)
	if unauthenticated.Code != http.StatusUnauthorized {
		t.Fatalf("unauthenticated status=%d body=%s", unauthenticated.Code, unauthenticated.Body.String())
	}
}

func TestUIAppsFailClosedOnMixedOrWrongAuthority(t *testing.T) {
	base := uiAppsTestView(t, uiAppsTestNavigation)
	tests := []struct {
		name        string
		mutateView  func(*catalogsemantic.AppMetadataViewV3)
		mutatePerms func(*permissionsemantic.Response)
	}{
		{name: "catalog revision", mutatePerms: func(value *permissionsemantic.Response) { value.Receipts.CatalogRevision++ }},
		{name: "security epoch", mutatePerms: func(value *permissionsemantic.Response) { value.Receipts.SecurityEpoch++ }},
		{name: "schema generation", mutatePerms: func(value *permissionsemantic.Response) { value.Receipts.SchemaGeneration++ }},
		{name: "engine generation", mutatePerms: func(value *permissionsemantic.Response) { value.Receipts.EngineGeneration++ }},
		{name: "wrong app", mutateView: func(value *catalogsemantic.AppMetadataViewV3) {
			value.AppID = "io.nexuscrm.other"
			value.Lineage.ViewDigest = catalogsemantic.ComputeAppMetadataViewV3Digest(*value)
		}},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			view := base
			permissions := uiAppsTestPermissions(view)
			if test.mutateView != nil {
				test.mutateView(&view)
			}
			if test.mutatePerms != nil {
				test.mutatePerms(&permissions)
			}
			service := uiAppsTestService(t, view, permissions)
			recorder := httptest.NewRecorder()
			service.ServeHTTP(recorder, uiAppsRequest(http.MethodGet, "/services/data/v63.0/ui-api/apps"))
			if recorder.Code == http.StatusOK {
				t.Fatalf("mixed/wrong authority returned success: %s", recorder.Body.String())
			}
		})
	}
}

func TestUIAppsReconcilesIndependentSecurityEpoch(t *testing.T) {
	view := uiAppsTestView(t, uiAppsTestNavigation)
	permissions := uiAppsTestPermissions(view)
	permissions.Receipts.SecurityEpoch++
	config := permissionMetadataServiceConfig(
		func(_ context.Context, _ string, _ permissionsemantic.Request) (permissionsemantic.Response, error) {
			return permissions, nil
		},
		func(context.Context, catalogsemantic.GetRequest) (catalogsemantic.AppMetadataView, error) {
			return validSearchScopeView(t, view.Receipts.CatalogRevision, view.Receipts.SecurityEpoch), nil
		},
	)
	config.RuntimeGetAppMetadataViewV3 = func(context.Context, catalogsemantic.GetRequest) (catalogsemantic.AppMetadataViewV3, error) {
		return view, nil
	}
	reconcileCalls := 0
	config.RuntimeReconcileAppMetadataViewV3 = func(
		_ context.Context,
		expected commonsemantic.Receipts,
		actual commonsemantic.Receipts,
	) (bool, error) {
		reconcileCalls++
		if expected.SecurityEpoch != view.Receipts.SecurityEpoch ||
			actual.SecurityEpoch != permissions.Receipts.SecurityEpoch {
			t.Fatalf("reconcile receipts = %#v -> %#v", expected, actual)
		}
		return true, nil
	}
	config.EnableUIApps = true
	config.RuntimeResolveApplicationNavigation = uiAppsTestCurrentNavigation(view, permissions)
	service, err := NewService(config)
	if err != nil {
		t.Fatal(err)
	}
	recorder := httptest.NewRecorder()
	service.ServeHTTP(recorder, uiAppsRequest(http.MethodGet, "/services/data/v63.0/ui-api/apps"))
	if recorder.Code != http.StatusOK {
		t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
	}
	if reconcileCalls != 1 {
		t.Fatalf("reconcile calls = %d, want 1", reconcileCalls)
	}
}

func TestUIAppsRejectsMalformedImmutableProjection(t *testing.T) {
	tests := []struct {
		name     string
		behavior string
	}{
		{name: "malformed json", behavior: `{"applications":[`},
		{name: "unknown root field", behavior: `{"applications":[],"future":true}`},
		{name: "missing applications", behavior: `{}`},
		{name: "null applications", behavior: `{"applications":null}`},
		{name: "duplicate application", behavior: `{"applications":[{"id":"Sales","label":"Sales","navigationItems":[],"utilityItems":[]},{"id":"sales","label":"Other","navigationItems":[],"utilityItems":[]}]}`},
		{name: "unknown navigation kind", behavior: `{"applications":[{"id":"Sales","label":"Sales","navigationItems":[{"id":"bad","type":"future","label":"Bad"}],"utilityItems":[]}]}`},
		{name: "duplicate navigation", behavior: `{"applications":[{"id":"Sales","label":"Sales","navigationItems":[{"id":"A","type":"page","label":"A"},{"id":"a","type":"page","label":"B"}],"utilityItems":[]}]}`},
		{name: "duplicate utility", behavior: `{"applications":[{"id":"Sales","label":"Sales","navigationItems":[],"utilityItems":[{"id":"U","type":"history","label":"U"},{"id":"u","type":"history","label":"V"}]}]}`},
		{name: "malformed hidden application", behavior: `{"applications":[{"id":"Sales","label":"Sales","navigationItems":[],"utilityItems":[]},{"id":"Service","label":"Service","navigationItems":[{"id":"bad","type":"future","label":"Bad"}],"utilityItems":[]}]}`},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			view := uiAppsTestView(t, test.behavior)
			service := uiAppsTestService(t, view, uiAppsTestPermissions(view))
			recorder := httptest.NewRecorder()
			service.ServeHTTP(recorder, uiAppsRequest(http.MethodGet, "/services/data/v63.0/ui-api/apps"))
			if recorder.Code == http.StatusOK {
				t.Fatalf("malformed projection returned success: %s", recorder.Body.String())
			}
		})
	}
}

func TestUIAppsPreservesCancellationAndDeadlineClassification(t *testing.T) {
	view := uiAppsTestView(t, uiAppsTestNavigation)
	for _, test := range []struct {
		name   string
		err    error
		status int
	}{
		{name: "canceled", err: context.Canceled, status: 499},
		{name: "deadline", err: context.DeadlineExceeded, status: http.StatusGatewayTimeout},
	} {
		t.Run(test.name, func(t *testing.T) {
			config := permissionMetadataServiceConfig(
				func(context.Context, string, permissionsemantic.Request) (permissionsemantic.Response, error) {
					t.Fatal("permissions called after Catalog failure")
					return permissionsemantic.Response{}, nil
				},
				func(context.Context, catalogsemantic.GetRequest) (catalogsemantic.AppMetadataView, error) {
					return validSearchScopeView(t, view.Receipts.CatalogRevision, view.Receipts.SecurityEpoch), nil
				},
			)
			config.RuntimeGetAppMetadataViewV3 = func(context.Context, catalogsemantic.GetRequest) (catalogsemantic.AppMetadataViewV3, error) {
				return catalogsemantic.AppMetadataViewV3{}, test.err
			}
			config.EnableUIApps = true
			config.RuntimeResolveApplicationNavigation = uiAppsTestCurrentNavigation(view, uiAppsTestPermissions(view))
			service, err := NewService(config)
			if err != nil {
				t.Fatal(err)
			}
			recorder := httptest.NewRecorder()
			service.ServeHTTP(recorder, uiAppsRequest(http.MethodGet, "/services/data/v63.0/ui-api/apps"))
			if recorder.Code != test.status {
				t.Fatalf("status=%d want=%d body=%s", recorder.Code, test.status, recorder.Body.String())
			}
		})
	}
}

// Fixture conversion supplies the independently current Runtime document. It is
// test data assembly, never a production fallback to Catalog navigation.
func uiAppsTestCurrentNavigation(view catalogsemantic.AppMetadataViewV3, permissions permissionsemantic.Response) RuntimeResolveApplicationNavigationFunc {
	return func(ctx context.Context, _ string, request navigationsemantic.ResolveRequest) (navigationsemantic.ResolveResponse, error) {
		definitions, err := decodeApplicationDefinitions(view)
		if err != nil {
			return navigationsemantic.ResolveResponse{}, err
		}
		documents := make([]navigationsemantic.Document, 0)
		visibleApps := map[string]bool{}
		for _, p := range permissions.ApplicationPermissions {
			visibleApps[strings.ToLower(p.ApplicationID)] = p.CanView
		}
		tabs := map[string]bool{}
		for _, p := range permissions.NavigationPermissions {
			tabs[strings.ToLower(p.NavigationKey)] = p.CanView
		}
		for _, app := range definitions.Applications {
			if !validEffectiveNavigationApplication(app) {
				return navigationsemantic.ResolveResponse{}, unavailableUIApps()
			}
			if !visibleApps[strings.ToLower(app.ID)] {
				continue
			}
			doc := navigationsemantic.Document{ApplicationID: app.ID, Revision: 1, Items: []navigationsemantic.Item{}}
			for _, item := range app.NavigationItems {
				next := navigationsemantic.Item{ID: item.ID, Label: item.Label, Icon: item.Icon}
				switch item.Type {
				case "object":
					if !tabs[strings.ToLower(item.ObjectAPIName)] {
						continue
					}
					next.Target.Resource = &navigationsemantic.ResourceTarget{ResourceType: item.ObjectAPIName}
				case "page":
					key := item.ObjectAPIName
					if key == "" {
						key = item.Label
					}
					if !tabs[strings.ToLower(key)] {
						continue
					}
					next.Target.Page = &navigationsemantic.PageTarget{ID: key}
				case "web":
					value := item.PageURL
					next.Target.URL = &value
				case "dashboard":
					next.Target.Resource = &navigationsemantic.ResourceTarget{ResourceType: "Dashboard", ResourceID: item.DashboardID}
				}
				doc.Items = append(doc.Items, next)
			}
			documents = append(documents, doc)
		}
		response := navigationsemantic.ResolveResponse{Documents: documents, Receipts: commonsemantic.Receipts{}}
		response.Receipts.ProtocolVersion = "1.0"
		response.Receipts.SecurityEpoch = permissions.Receipts.SecurityEpoch
		response.Receipts.CapabilityVersion = navigationsemantic.CapabilityName
		return response, nil
	}
}

func TestUIAppsReadsChangedNavigationWithFrozenCatalog(t *testing.T) {
	view := uiAppsTestView(t, uiAppsTestNavigation)
	permissions := uiAppsTestPermissions(view)
	service := uiAppsTestService(t, view, permissions)
	url := "https://example.test/changed"
	current := []navigationsemantic.Item{{ID: "new-link", Label: "Changed immediately", Target: navigationsemantic.Target{URL: &url}}}
	revision := uint64(9)
	service.uiApps.resolveNavigation = func(_ context.Context, _ string, request navigationsemantic.ResolveRequest) (navigationsemantic.ResolveResponse, error) {
		if !slices.Equal(request.ApplicationIDs, []string{"Sales", "Service"}) {
			t.Fatalf("application scope=%v", request.ApplicationIDs)
		}
		return navigationsemantic.ResolveResponse{Documents: []navigationsemantic.Document{{ApplicationID: "Sales", Revision: revision, Items: current}}, Receipts: commonsemantic.Receipts{ProtocolVersion: "1.0", SecurityEpoch: permissions.Receipts.SecurityEpoch, CapabilityVersion: navigationsemantic.CapabilityName}}, nil
	}
	first := httptest.NewRecorder()
	service.ServeHTTP(first, uiAppsRequest(http.MethodGet, "/services/data/v63.0/ui-api/apps"))
	if first.Code != http.StatusOK || !strings.Contains(first.Body.String(), "Changed immediately") || strings.Contains(first.Body.String(), "Documentation") || strings.Contains(first.Body.String(), "account") {
		t.Fatalf("read retained Catalog navigation: %d %s", first.Code, first.Body.String())
	}
	current = []navigationsemantic.Item{}
	revision++
	second := httptest.NewRecorder()
	service.ServeHTTP(second, uiAppsRequest(http.MethodGet, "/services/data/v63.0/ui-api/apps"))
	var result struct {
		Apps map[string]uiApp `json:"apps"`
	}
	if second.Code != http.StatusOK || json.Unmarshal(second.Body.Bytes(), &result) != nil {
		t.Fatalf("empty read: %d %s", second.Code, second.Body.String())
	}
	app, exists := result.Apps["Sales"]
	if !exists || app.NavItems == nil || len(app.NavItems) != 0 {
		t.Fatalf("empty navigation fell back to Catalog: %+v", result)
	}
}
