//go:build crm

package crm

import (
	"context"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

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

func newTestAdmin(rec *fakeRecords, mint func(context.Context, string, string) (string, error)) *scimAdminHandler {
	cfg := testScimConfig()
	return &scimAdminHandler{
		recordsFn:     func() contract.Records { return rec },
		currentCfg:    func() scimIdentityConfig { return cfg },
		state:         newScimIdentityState(time.Now().UTC(), 5, 300),
		callerSession: func(*gin.Context) *models.UserSession { return testSession() },
		mintToken:     mint,
	}
}

func adminRouter(a *scimAdminHandler) *gin.Engine {
	gin.SetMode(gin.TestMode)
	r := gin.New()
	g := r.Group("/services/identity-sync")
	g.POST("/scim-token", a.mintTokenHandler)
	g.GET("/status", a.status)
	g.GET("/breaker", a.breaker)
	g.POST("/breaker/reset", a.resetBreaker)
	g.GET("/journal", a.journal)
	g.GET("/quarantine", a.quarantine)
	return r
}

func TestScimAdmin_MintTokenBootstrapsServiceUser(t *testing.T) {
	rec := newFakeRecords()
	rec.put(contract.Record{"Id": "PROFILE_SYS", scimFieldName: "System Administrator"})
	var mintedOwner string
	a := newTestAdmin(rec, func(_ context.Context, ownerID, _ string) (string, error) {
		mintedOwner = ownerID
		return "nxc_faketoken", nil
	})
	r := adminRouter(a)

	w := do(r, "POST", "/services/identity-sync/scim-token", "")
	if w.Code != 201 {
		t.Fatalf("mint code=%d body=%s", w.Code, w.Body.String())
	}
	m := decode(t, w)
	if m["token"] != "nxc_faketoken" {
		t.Errorf("token not returned: %v", m["token"])
	}
	// The service user must have been created (SSO-only, no password).
	if rec.inserted == nil {
		t.Fatal("service user was not created")
	}
	if _, leaked := rec.inserted["Password"]; leaked {
		t.Error("service user must not have a password")
	}
	if mintedOwner != rec.inserted["Id"] {
		t.Errorf("token owner %q != created service user %v", mintedOwner, rec.inserted["Id"])
	}

	// A second mint reuses the existing service user (no duplicate create).
	rec.inserted = nil
	w = do(r, "POST", "/services/identity-sync/scim-token", "")
	if w.Code != 201 {
		t.Fatalf("second mint code=%d", w.Code)
	}
	if rec.inserted != nil {
		t.Error("second mint must reuse the existing service user, not create another")
	}
}

func TestScimAdmin_MintReactivatesInactiveServiceUser(t *testing.T) {
	// A deactivated service account would make every minted token unusable
	// (token validation rejects an inactive owner); the mint is the
	// admin-facing repair path and must reactivate it.
	rec := newFakeRecords()
	rec.put(contract.Record{"Id": "PROFILE_SYS", scimFieldName: "System Administrator"})
	cfg := testScimConfig()
	rec.put(contract.Record{"Id": "005SVCX", scim.FieldUsername: cfg.serviceUsername,
		scim.FieldProfileID: "PROFILE_SYS", scim.FieldIsActive: false})
	a := newTestAdmin(rec, func(context.Context, string, string) (string, error) {
		return "nxc_faketoken", nil
	})
	w := do(adminRouter(a), "POST", "/services/identity-sync/scim-token", "")
	if w.Code != 201 {
		t.Fatalf("mint code=%d body=%s", w.Code, w.Body.String())
	}
	if rec.byID["005SVCX"][scim.FieldIsActive] != true {
		t.Fatal("mint must reactivate an inactive service account")
	}
}

func TestScimAdmin_MintRefusesServiceUserWithElevatedGrants(t *testing.T) {
	// A pre-existing account at the service username whose PROFILE matches but
	// that carries permission-set assignments or group memberships (privileges
	// stacked on top of the profile) must be refused — a SCIM bearer must never
	// inherit grants beyond the least-privilege service profile.
	cfg := testScimConfig()
	for _, grant := range []struct {
		object, field string
	}{
		{constants.TablePermissionSetAssignment, constants.FieldPermissionSetAssignment_AssigneeID},
		{constants.TableGroupMember, constants.FieldGroupMember_UserOrGroupID},
	} {
		rec := newFakeRecords()
		rec.put(contract.Record{"Id": "PROFILE_SYS", scimFieldName: "System Administrator"})
		rec.put(contract.Record{"Id": "005SVCX", scim.FieldUsername: cfg.serviceUsername,
			scim.FieldProfileID: "PROFILE_SYS", scim.FieldIsActive: true})
		// Seed an elevated grant keyed by the service user's id.
		rec.putUnderField(grant.object, grant.field, "005SVCX",
			contract.Record{"Id": "GRANT1", grant.field: "005SVCX"})
		a := newTestAdmin(rec, func(context.Context, string, string) (string, error) {
			return "nxc_faketoken", nil
		})
		w := do(adminRouter(a), "POST", "/services/identity-sync/scim-token", "")
		if w.Code == 201 {
			t.Fatalf("%s: mint must refuse a service account with elevated grants, got 201", grant.object)
		}
		if got := w.Header().Get("Content-Type"); got != "application/json; charset=utf-8" {
			t.Fatalf("%s: mint failure content type=%q, want ordinary JSON for the CRM setup client", grant.object, got)
		}
	}
}

func TestScimAdmin_StatusReflectsConfigAndBreaker(t *testing.T) {
	rec := newFakeRecords()
	a := newTestAdmin(rec, nil)
	r := adminRouter(a)

	w := do(r, "GET", "/services/identity-sync/status", "")
	if w.Code != 200 {
		t.Fatalf("status code=%d", w.Code)
	}
	if got := w.Header().Get("Content-Type"); got != "application/json; charset=utf-8" {
		t.Fatalf("status content type=%q, want ordinary JSON for the CRM setup client", got)
	}
	m := decode(t, w)
	breaker, _ := m["breaker"].(map[string]any)
	if breaker["status"] != "closed" {
		t.Errorf("fresh breaker should be closed, got %v", breaker["status"])
	}
	cfg, _ := m["config"].(map[string]any)
	if cfg["defaultProfile"] != "Standard User" {
		t.Errorf("config defaultProfile = %v", cfg["defaultProfile"])
	}
}

func TestScimAdmin_BreakerTripsAndResets(t *testing.T) {
	a := newTestAdmin(newFakeRecords(), nil)
	now := time.Now().UTC()
	for i := 0; i < 5; i++ {
		a.state.onDeactivateFailure(now, "boom")
	}
	if s, _ := a.state.breakerStatus(); s != "open" {
		t.Fatalf("breaker should be open after threshold failures, got %s", s)
	}
	if a.state.reserveDeactivation(now) {
		t.Error("open breaker (within cooldown) must block deactivation")
	}
	// cooldown elapsed -> exactly ONE half-open trial is admitted (reserved).
	later := now.Add(6 * time.Minute)
	if !a.state.reserveDeactivation(later) {
		t.Error("after cooldown the breaker should admit one half-open trial")
	}
	if a.state.reserveDeactivation(later) {
		t.Error("a second concurrent half-open trial must be blocked (reservation)")
	}
	r := adminRouter(a)
	if w := do(r, "POST", "/services/identity-sync/breaker/reset", ""); w.Code != 200 {
		t.Fatalf("reset code=%d", w.Code)
	}
	if s, _ := a.state.breakerStatus(); s != "closed" {
		t.Errorf("breaker should be closed after reset, got %s", s)
	}
}

func TestScimAdmin_JournalRecordsAndPaginates(t *testing.T) {
	a := newTestAdmin(newFakeRecords(), nil)
	now := time.Now().UTC()
	for i := 0; i < 3; i++ {
		a.state.record(now, "provisioned", "u@pingcap.com", "svc@pingcap.com", "created")
	}
	r := adminRouter(a)
	w := do(r, "GET", "/services/identity-sync/journal?limit=2&offset=0", "")
	if w.Code != 200 {
		t.Fatalf("journal code=%d", w.Code)
	}
	if got := w.Header().Get("Content-Type"); got != "application/json; charset=utf-8" {
		t.Fatalf("journal content type=%q, want ordinary JSON for the CRM setup client", got)
	}
	m := decode(t, w)
	if int(m["total"].(float64)) != 3 {
		t.Errorf("journal total = %v, want 3", m["total"])
	}
	events, _ := m["events"].([]any)
	if len(events) != 2 {
		t.Errorf("journal page = %d, want 2", len(events))
	}
}

func TestWriteScimAdminJSONMarshalFailureUsesOrdinaryJSON(t *testing.T) {
	gin.SetMode(gin.TestMode)
	r := gin.New()
	r.GET("/", func(c *gin.Context) {
		writeScimAdminJSON(c, http.StatusOK, map[string]any{"bad": make(chan int)})
	})
	w := do(r, http.MethodGet, "/", "")
	if w.Code != http.StatusInternalServerError {
		t.Fatalf("status=%d, want %d", w.Code, http.StatusInternalServerError)
	}
	if got := w.Header().Get("Content-Type"); got != "application/json; charset=utf-8" {
		t.Fatalf("content type=%q, want ordinary JSON", got)
	}
}

var _ = httptest.NewRequest // keep httptest referenced if helpers change

func TestScim_RenderEnterpriseSchema(t *testing.T) {
	h, _, _ := newTestHandler()
	rec := contract.Record{
		"Id": "005E", scim.FieldUsername: "e@pingcap.com", scim.FieldEmail: "e@pingcap.com",
		scim.FieldIsActive: true, scim.FieldDepartment: "Eng",
		"Division": "legacy division", "CompanyName": "legacy organization",
	}
	res := h.renderUser(rec)
	ent, ok := res[scim.SchemaEnterprise].(map[string]any)
	if !ok || ent["department"] != "Eng" {
		t.Errorf("enterprise render wrong: %+v", res)
	}
	if _, found := ent["division"]; found {
		t.Errorf("division must not be rendered: %+v", res)
	}
	if _, found := ent["organization"]; found {
		t.Errorf("organization must not be rendered: %+v", res)
	}
	schemas, _ := res["schemas"].([]string)
	found := false
	for _, s := range schemas {
		if s == scim.SchemaEnterprise {
			found = true
		}
	}
	if !found {
		t.Error("enterprise schema URN must be declared when enterprise fields present")
	}
}
