package dialectsfcrm

import (
	"bytes"
	"context"
	"errors"
	"log"
	"net/http"
	"strings"
	"testing"

	"github.com/nexuscrm/dialect-sf-crm/wire"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/contract"
	"github.com/nexuscrm/shared/pkg/dialect"
	"github.com/nexuscrm/shared/pkg/models"
)

func TestListUIApplicationsProjectsVisibleAppsAndEnrichesObjectLabels(t *testing.T) {
	appNav := &fakeApplicationNavigation{
		visible: []*models.AppConfig{{
			ID:            "Sales",
			FrontendAppID: "crm",
			Label:         "Sales Console",
			IsDefault:     true,
			NavigationItems: []models.NavigationItem{
				{ID: "nav-account", Type: constants.NavigationTypeObject, ObjectAPIName: "account", Label: "account"},
			},
		}},
	}
	resp, err := ListUIApplicationsNeutralHandler(context.Background(), applicationRequest(), applicationDeps(appNav, applicationMetadata()))
	if err != nil {
		t.Fatalf("handler returned error: %v", err)
	}
	if resp.Status != http.StatusOK {
		t.Fatalf("status = %d, want 200; body=%#v", resp.Status, resp.Body)
	}
	want := `{"apps":{"Sales":{"appId":"Sales","frontendAppId":"crm","developerName":"Sales","formFactors":["Large"],"isNavPersonalizationDisabled":false,"label":"Sales Console","navItems":[{"id":"nav-account","developerName":"nav-account","label":"Accounts","objectApiName":"Account","itemType":"object"}],"selected":true}}}`
	assertBody(t, "ui apps", resp.Body, want, want)
}

func TestToolingCustomApplicationCreateParsesTabsAndEnrichesLabels(t *testing.T) {
	appNav := &fakeApplicationNavigation{}
	resp, err := ToolingCustomApplicationCreateNeutralHandler(context.Background(), &dialect.Request{
		Body: []byte(`{"FullName":"Support","Metadata":{"label":"Support App","frontendAppId":"crm","tabs":["standard-home","standard-account"]}}`),
	}, applicationDeps(appNav, applicationMetadata()))
	if err != nil {
		t.Fatalf("handler returned error: %v", err)
	}
	if resp.Status != http.StatusCreated {
		t.Fatalf("status = %d, want 201; body=%#v", resp.Status, resp.Body)
	}
	if appNav.created == nil {
		t.Fatal("CreateApplication was not called")
	}
	if appNav.created.ID != "Support" || appNav.created.Label != "Support App" {
		t.Fatalf("created app = %+v, want Support App", appNav.created)
	}
	if appNav.created.FrontendAppID != "crm" {
		t.Fatalf("created FrontendAppID = %q, want crm", appNav.created.FrontendAppID)
	}
	if len(appNav.created.NavigationItems) != 2 {
		t.Fatalf("created nav len = %d, want 2", len(appNav.created.NavigationItems))
	}
	if first := appNav.created.NavigationItems[0]; first.Type != constants.NavigationTypePage || first.ObjectAPIName != "Home" {
		t.Fatalf("first nav = %+v, want Home page tab", first)
	}
	if second := appNav.created.NavigationItems[1]; second.Type != constants.NavigationTypeObject || second.ObjectAPIName != "Account" || second.Label != "Accounts" {
		t.Fatalf("second nav = %+v, want enriched Account object tab", second)
	}
	want := `{"errors":[],"id":"Support","success":true}`
	assertBody(t, "create response", resp.Body, want, want)
}

func TestToolingCustomApplicationUpdatePassesTabsForAtomicMerge(t *testing.T) {
	appNav := &fakeApplicationNavigation{}
	resp, err := ToolingCustomApplicationUpdateNeutralHandler(context.Background(), &dialect.Request{
		Params: map[string]string{"id": "Sales"},
		Body:   []byte(`{"Metadata":{"frontendAppId":"crm","tabs":["web-link-1","standard-account"]}}`),
	}, applicationDeps(appNav, applicationMetadata()))
	if err != nil {
		t.Fatalf("handler returned error: %v", err)
	}
	if resp.Status != http.StatusNoContent {
		t.Fatalf("status = %d, want 204; body=%#v", resp.Status, resp.Body)
	}
	if appNav.updatedID != "Sales" || appNav.updated == nil {
		t.Fatalf("updatedID=%q updated=%+v, want Sales update", appNav.updatedID, appNav.updated)
	}
	if appNav.getCalled {
		t.Fatal("GetApplication was called; tab merge must stay under the application navigation owner")
	}
	if appNav.updated.FrontendAppID != "crm" {
		t.Fatalf("updated FrontendAppID = %q, want crm", appNav.updated.FrontendAppID)
	}
	if got := appNav.updatePresence.NavigationItemIdentities; len(got) != 2 ||
		got[0].Kind != contract.ApplicationNavigationItemIdentityID || got[0].Key != "web-link-1" ||
		got[1].Kind != contract.ApplicationNavigationItemIdentityObject || got[1].Key != "Account" {
		t.Fatalf("merge identities = %#v, want neutral id/object identities", got)
	}
	got := appNav.updated.NavigationItems
	if len(got) != 2 {
		t.Fatalf("updated nav len = %d, want 2", len(got))
	}
	if got[0].Type != constants.NavigationTypePage || got[0].Label != "web-link-1" {
		t.Fatalf("first nav = %+v, want parsed web-link tab placeholder", got[0])
	}
	if got[1].Type != constants.NavigationTypeObject || got[1].ObjectAPIName != "Account" || got[1].Label != "Accounts" {
		t.Fatalf("second nav = %+v, want enriched Account object tab", got[1])
	}
}

func TestToolingCustomApplicationGetReturnsFrontendAppOwner(t *testing.T) {
	resp, err := ToolingCustomApplicationGetNeutralHandler(context.Background(), &dialect.Request{
		Params: map[string]string{"id": "Sales"},
	}, applicationDeps(&fakeApplicationNavigation{
		app: &models.AppConfig{
			ID:            "Sales",
			FrontendAppID: "crm",
			Label:         "Sales Console",
			NavigationItems: []models.NavigationItem{
				{ID: "nav-home", Type: constants.NavigationTypePage, ObjectAPIName: "Home", Label: "Home"},
			},
		},
	}, applicationMetadata()))
	if err != nil {
		t.Fatalf("handler returned error: %v", err)
	}
	if resp.Status != http.StatusOK {
		t.Fatalf("status = %d, want 200; body=%#v", resp.Status, resp.Body)
	}
	want := `{"FullName":"Sales","Id":"Sales","Metadata":{"description":"","frontendAppId":"crm","label":"Sales Console","tabs":["standard-home"]}}`
	assertBody(t, "tooling get", resp.Body, want, want)
}

func TestToolingCustomApplicationGetMissingUsesNotFoundEnvelope(t *testing.T) {
	resp, err := ToolingCustomApplicationGetNeutralHandler(context.Background(), &dialect.Request{
		Params: map[string]string{"id": "Missing"},
	}, applicationDeps(&fakeApplicationNavigation{}, applicationMetadata()))
	if err != nil {
		t.Fatalf("handler returned error: %v", err)
	}
	want := mustJSON(t, wire.SFErrorResponse(wire.SFCodeNotFound, "Application not found"))
	assertBody(t, "not found", resp.Body, want, want)
}

func TestApplicationNavigationInternalErrorsAreSanitized(t *testing.T) {
	sensitive := "dial tcp 10.0.0.12:3306 password=super-secret SELECT * FROM _System_App"
	logOutput := captureApplicationNavigationLogs(t)
	resp, err := ToolingCustomApplicationListNeutralHandler(context.Background(), applicationRequest(), applicationDeps(&fakeApplicationNavigation{
		listErr: errors.New(sensitive),
	}, applicationMetadata()))
	if err != nil {
		t.Fatalf("handler returned error: %v", err)
	}
	if resp.Status != http.StatusInternalServerError {
		t.Fatalf("status = %d, want 500; body=%#v", resp.Status, resp.Body)
	}
	want := mustJSON(t, wire.SFErrorResponse(wire.SFCodeInternalError, "An internal server error occurred. Contact your system administrator."))
	assertBody(t, "internal error", resp.Body, want, want)
	raw := mustJSON(t, resp.Body)
	for _, leaked := range []string{"super-secret", "SELECT * FROM", "10.0.0.12", "_System_App"} {
		if strings.Contains(raw, leaked) {
			t.Fatalf("internal error response leaked %q: %s", leaked, raw)
		}
	}
	if !strings.Contains(logOutput.String(), sensitive) {
		t.Fatalf("log output = %q, want sensitive cause logged", logOutput.String())
	}
}

func TestApplicationNavigationMissingDependenciesAreSanitizedAndLogged(t *testing.T) {
	logOutput := captureApplicationNavigationLogs(t)
	resp, err := ToolingCustomApplicationListNeutralHandler(context.Background(), applicationRequest(), applicationDeps(nil, applicationMetadata()))
	if err != nil {
		t.Fatalf("handler returned error: %v", err)
	}
	assertApplicationNavigationInternalResponse(t, "missing app navigation", resp, "application navigation service")
	if !strings.Contains(logOutput.String(), "application navigation contract unavailable") {
		t.Fatalf("log output = %q, want missing application navigation dependency", logOutput.String())
	}

	logOutput.Reset()
	resp, err = ListUIApplicationsNeutralHandler(context.Background(), applicationRequest(), applicationDeps(&fakeApplicationNavigation{
		visible: []*models.AppConfig{{
			ID: "Sales",
			NavigationItems: []models.NavigationItem{
				{ID: "nav-account", Type: constants.NavigationTypeObject, ObjectAPIName: "Account", Label: "Account"},
			},
		}},
	}, nil))
	if err != nil {
		t.Fatalf("handler returned error: %v", err)
	}
	assertApplicationNavigationInternalResponse(t, "missing metadata", resp, "metadata service")
	if !strings.Contains(logOutput.String(), "metadata contract unavailable") {
		t.Fatalf("log output = %q, want missing metadata dependency", logOutput.String())
	}
}

func TestApplicationNavigationContractFiveHundredsAreSanitizedAndLogged(t *testing.T) {
	for _, kind := range []contract.ApplicationNavigationErrorKind{
		contract.ApplicationNavigationErrorInternal,
		contract.ApplicationNavigationErrorServer,
	} {
		t.Run(string(kind), func(t *testing.T) {
			sensitive := "sql driver timeout password=nav-secret SELECT Id FROM CustomApplication"
			logOutput := captureApplicationNavigationLogs(t)
			resp, err := ToolingCustomApplicationListNeutralHandler(context.Background(), applicationRequest(), applicationDeps(&fakeApplicationNavigation{
				listErr: &contract.ApplicationNavigationError{Kind: kind, Message: sensitive},
			}, applicationMetadata()))
			if err != nil {
				t.Fatalf("handler returned error: %v", err)
			}
			assertApplicationNavigationInternalResponse(t, string(kind), resp, "nav-secret")
			if !strings.Contains(logOutput.String(), sensitive) {
				t.Fatalf("log output = %q, want sensitive cause logged", logOutput.String())
			}
		})
	}
}

func TestApplicationNavigationContractFiveHundredsLogWrappedCause(t *testing.T) {
	sensitive := "permissions lookup failed password=wrapped-secret SELECT * FROM _System_App"
	logOutput := captureApplicationNavigationLogs(t)
	resp, err := ToolingCustomApplicationListNeutralHandler(context.Background(), applicationRequest(), applicationDeps(&fakeApplicationNavigation{
		listErr: &contract.ApplicationNavigationError{
			Kind:    contract.ApplicationNavigationErrorInternal,
			Message: applicationNavigationGenericInternalErrorMessage,
			Cause:   errors.New(sensitive),
		},
	}, applicationMetadata()))
	if err != nil {
		t.Fatalf("handler returned error: %v", err)
	}
	assertApplicationNavigationInternalResponse(t, "wrapped cause", resp, "wrapped-secret")
	if !strings.Contains(logOutput.String(), sensitive) {
		t.Fatalf("log output = %q, want wrapped cause logged", logOutput.String())
	}
}

func TestApplicationNavigationMetadataErrorsAreSanitizedAndLogged(t *testing.T) {
	sensitive := "metadata cache failed password=metadata-secret SELECT * FROM _System_Object"
	logOutput := captureApplicationNavigationLogs(t)
	resp, err := ListUIApplicationsNeutralHandler(context.Background(), applicationRequest(), applicationDeps(&fakeApplicationNavigation{
		visible: []*models.AppConfig{{
			ID: "Sales",
			NavigationItems: []models.NavigationItem{
				{ID: "nav-account", Type: constants.NavigationTypeObject, ObjectAPIName: "Account", Label: "Account"},
			},
		}},
	}, &fakeApplicationMetadata{err: errors.New(sensitive)}))
	if err != nil {
		t.Fatalf("handler returned error: %v", err)
	}
	assertApplicationNavigationInternalResponse(t, "metadata error", resp, "metadata-secret")
	if !strings.Contains(logOutput.String(), sensitive) {
		t.Fatalf("log output = %q, want sensitive cause logged", logOutput.String())
	}
}

func applicationRequest() *dialect.Request {
	return &dialect.Request{
		User:   &models.UserSession{ID: "005xx0000000001AAA", ProfileID: "00exx0000000001AAA"},
		Params: map[string]string{"id": "Sales"},
	}
}

func applicationDeps(appNav contract.ApplicationNavigation, metadata contract.Metadata) dialect.Deps {
	return dialect.Deps{Contracts: contract.Deps{ApplicationNavigation: appNav, Metadata: metadata}}
}

func applicationMetadata() contract.Metadata {
	return &fakeApplicationMetadata{objects: []models.ObjectMetadata{{
		APIName:     "Account",
		Label:       "Account",
		PluralLabel: "Accounts",
	}}}
}

func captureApplicationNavigationLogs(t *testing.T) *bytes.Buffer {
	t.Helper()
	var output bytes.Buffer
	oldWriter := log.Writer()
	oldFlags := log.Flags()
	log.SetOutput(&output)
	log.SetFlags(0)
	t.Cleanup(func() {
		log.SetOutput(oldWriter)
		log.SetFlags(oldFlags)
	})
	return &output
}

func assertApplicationNavigationInternalResponse(t *testing.T, label string, resp *dialect.Response, forbiddenSubstrings ...string) {
	t.Helper()
	if resp.Status != http.StatusInternalServerError {
		t.Fatalf("%s status = %d, want 500; body=%#v", label, resp.Status, resp.Body)
	}
	want := mustJSON(t, wire.SFErrorResponse(wire.SFCodeInternalError, applicationNavigationGenericInternalErrorMessage))
	assertBody(t, label, resp.Body, want, want)
	raw := mustJSON(t, resp.Body)
	for _, leaked := range forbiddenSubstrings {
		if strings.Contains(raw, leaked) {
			t.Fatalf("%s response leaked %q: %s", label, leaked, raw)
		}
	}
}

type fakeApplicationNavigation struct {
	list           []*models.AppConfig
	visible        []*models.AppConfig
	app            *models.AppConfig
	listErr        error
	visibleErr     error
	getErr         error
	createErr      error
	updateErr      error
	deleteErr      error
	created        *models.AppConfig
	updatedID      string
	updated        *models.AppConfig
	getCalled      bool
	updatePresence contract.ApplicationPatchPresence
	deletedID      string
}

func (f *fakeApplicationNavigation) ListApplications(context.Context) ([]*models.AppConfig, error) {
	if f.listErr != nil {
		return nil, f.listErr
	}
	return f.list, nil
}

func (f *fakeApplicationNavigation) ListVisibleApplications(context.Context, *models.UserSession) ([]*models.AppConfig, error) {
	if f.visibleErr != nil {
		return nil, f.visibleErr
	}
	return f.visible, nil
}

func (f *fakeApplicationNavigation) GetApplication(context.Context, string) (*models.AppConfig, error) {
	f.getCalled = true
	if f.getErr != nil {
		return nil, f.getErr
	}
	return f.app, nil
}

func (f *fakeApplicationNavigation) CreateApplication(_ context.Context, app *models.AppConfig) error {
	if f.createErr != nil {
		return f.createErr
	}
	f.created = app
	return nil
}

func (f *fakeApplicationNavigation) UpdateApplication(_ context.Context, id string, updates *models.AppConfig, presence contract.ApplicationPatchPresence) error {
	if f.updateErr != nil {
		return f.updateErr
	}
	f.updatedID = id
	f.updated = updates
	f.updatePresence = presence
	return nil
}

func (f *fakeApplicationNavigation) DeleteApplication(_ context.Context, id string) error {
	if f.deleteErr != nil {
		return f.deleteErr
	}
	f.deletedID = id
	return nil
}

type fakeApplicationMetadata struct {
	objects []models.ObjectMetadata
	err     error
}

func (*fakeApplicationMetadata) ResolvePicklistValuesForRecordType(context.Context, string, string, string) (*models.ScopedPicklistValueSet, error) {
	return nil, nil
}

func (f *fakeApplicationMetadata) Describe(context.Context, string) (*models.ObjectMetadata, error) {
	return nil, nil
}

func (f *fakeApplicationMetadata) ListObjects(context.Context) ([]models.ObjectMetadata, error) {
	if f.err != nil {
		return nil, f.err
	}
	return f.objects, nil
}

func (f *fakeApplicationMetadata) Relationships(context.Context, string) ([]*models.Relationship, error) {
	return nil, nil
}

func (f *fakeApplicationMetadata) ChildRelationships(context.Context, string) ([]*models.ObjectMetadata, error) {
	return nil, nil
}

func (f *fakeApplicationMetadata) KeyPrefix(context.Context, string) (string, bool) {
	return "", false
}

func (f *fakeApplicationMetadata) ResolveLayout(context.Context, string, *models.UserSession, string) (*models.PageLayout, error) {
	return nil, nil
}

func (f *fakeApplicationMetadata) ResolveRelatedListResolution(context.Context, string, string) (*contract.RelatedListResolution, bool) {
	return nil, false
}

func (f *fakeApplicationMetadata) Actions(context.Context, string) ([]*models.ActionMetadata, error) {
	return nil, nil
}

func (f *fakeApplicationMetadata) ActionByID(context.Context, string) (*models.ActionMetadata, error) {
	return nil, nil
}
