package dialecttransport_test

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

	"github.com/gin-gonic/gin"
	"github.com/pingcap-inc/nexuscrm-api-host/dialect"
	"github.com/pingcap-inc/nexuscrm-api-host/dialecttransport"
)

// pingDialect is a minimal registry-test partition: one unguarded GET <base>/<ver>/ping
// that returns 200, with a configurable Name + OwningPack so a test can vary the
// pack-gating facet.
func pingDialect(name, pack, base string) testDialect {
	return testDialect{
		name: name, pack: pack, base: base, vers: []string{"v1"},
		proj: notFoundProjector,
		manifest: []dialect.Route{
			{Method: http.MethodGet, Path: "/ping", Gate: dialect.Gate{Mode: dialect.GateNone},
				Handler: func(_ context.Context, _ *dialect.Request, _ dialect.Deps) (*dialect.Response, error) {
					return &dialect.Response{Status: http.StatusOK, Body: gin.H{"ok": true}}, nil
				}},
		},
	}
}

func mustPanic(t *testing.T, what string, fn func()) {
	t.Helper()
	defer func() {
		if r := recover(); r == nil {
			t.Fatalf("expected panic (%s), got none", what)
		}
	}()
	fn()
}

func TestRegisterIntoRejectsEmptyAndDuplicateName(t *testing.T) {
	dialecttransport.ResetForTest()
	t.Cleanup(dialecttransport.ResetForTest)

	mustPanic(t, "empty name", func() {
		dialecttransport.RegisterInto(pingDialect("", "p", "/x"))
	})

	dialecttransport.RegisterInto(pingDialect("dup", "p", "/x"))
	mustPanic(t, "duplicate name", func() {
		dialecttransport.RegisterInto(pingDialect("dup", "other", "/y"))
	})
}

func TestRegisteredIsSortedAndResettable(t *testing.T) {
	dialecttransport.ResetForTest()
	t.Cleanup(dialecttransport.ResetForTest)

	dialecttransport.RegisterInto(pingDialect("zebra", "p", "/z"))
	dialecttransport.RegisterInto(pingDialect("alpha", "p", "/a"))

	got := dialecttransport.Registered()
	if len(got) != 2 || got[0].Name() != "alpha" || got[1].Name() != "zebra" {
		t.Fatalf("Registered() = %v, want [alpha zebra] sorted", names(got))
	}

	dialecttransport.ResetForTest()
	if reg := dialecttransport.Registered(); len(reg) != 0 {
		t.Fatalf("after ResetForTest, Registered() = %v, want empty", names(reg))
	}
}

// TestMountRegisteredAllowsEmptyRegistry pins the app-blind contract (#2015 PR-0):
// the DEFAULT public-core build bundles NO product dialect, so the gin-free door's
// registry is legitimately empty and the generic drain must mount nothing WITHOUT
// panicking — a brand-new org boots with no app dialect. The fail-closed-on-broken-
// registration guard moved to the distribution build's bundle manifest
// (for CRM, cmd/crm-server), which asserts the expected partition is present right
// after registering it; MountRegistered itself no longer judges whether registration
// "should" have happened. (Greenfield-with-a-registered-pack — registry NON-empty,
// owning pack absent/inactive → skip at Mount — is covered by
// TestMountRegisteredGatesByActivePackGenerically.)
func TestMountRegisteredAllowsEmptyRegistry(t *testing.T) {
	gin.SetMode(gin.TestMode)
	dialecttransport.ResetForTest()
	t.Cleanup(dialecttransport.ResetForTest)

	if got := len(dialecttransport.Registered()); got != 0 {
		t.Fatalf("precondition: Registered() = %d, want 0", got)
	}

	router := gin.New()
	mounts := mountRegistered(router, deps(), base(), testUserFromContext, permsKnown(), map[string]bool{})
	if len(mounts) != 0 {
		t.Fatalf("empty registry: MountRegistered returned %d mounts, want 0 (app-blind: mount nothing)", len(mounts))
	}
	if routes := router.Routes(); len(routes) != 0 {
		t.Fatalf("empty registry: %d routes mounted, want 0 (app-blind: no gin-free dialect routes)", len(routes))
	}
}

func names(ds []dialect.Dialect) []string {
	out := make([]string, len(ds))
	for i, d := range ds {
		out[i] = d.Name()
	}
	return out
}

// TestMountRegisteredGatesByActivePackGenerically is the core: MountRegistered
// resolves each registered partition's OwningPack against the host-supplied
// packCatalog and mounts it iff that pack is active — without the host naming the
// pack. The catalog is now threaded in from the host (built once by
// packs.KnownInstalledPackCatalog), NOT derived inside MountRegistered from the
// partitions' own OwningPacks. It also pins greenfield-safety: a pack ABSENT from
// the catalog must SKIP via the binder's absent->skip (not the deleted self-seed,
// and not a panic), so a brand-new org boots.
func TestMountRegisteredGatesByActivePackGenerically(t *testing.T) {
	gin.SetMode(gin.TestMode)

	cases := []struct {
		name        string
		activePacks map[string]bool
		wantStatus  int // 200 mounted, 404 skipped
	}{
		{"pack active → mounted", map[string]bool{"test-pack": true}, http.StatusOK},
		{"pack absent (greenfield) → skipped, no panic", map[string]bool{}, http.StatusNotFound},
		{"pack inactive → skipped", map[string]bool{"test-pack": false}, http.StatusNotFound},
		{"pack active under case-variant → mounted (EqualFold)", map[string]bool{"Test-Pack": true}, http.StatusOK},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			dialecttransport.ResetForTest()
			t.Cleanup(dialecttransport.ResetForTest)
			dialecttransport.RegisterInto(pingDialect("test-dialect", "test-pack", "/svc"))

			router := gin.New()
			// Generic call: the host passes activePackNames and names NO pack. A nil panic
			// here (greenfield) would be the regression this test guards.
			mountRegistered(router, deps(), base(), testUserFromContext, permsKnown(), tc.activePacks)

			if w := serve(router, "/svc/v1/ping"); w.Code != tc.wantStatus {
				t.Fatalf("GET /svc/v1/ping status = %d, want %d (active=%v)", w.Code, tc.wantStatus, tc.activePacks)
			}
		})
	}
}

// TestMountRegisteredMountsSecondPackWithoutSpecialCasing is the whole point of the
// fix: a second app pack's partition mounts purely by registering itself, gated by
// its own pack, with zero host change. Two partitions owned by different packs, only
// one pack active → only that one mounts.
func TestMountRegisteredMountsSecondPackWithoutSpecialCasing(t *testing.T) {
	gin.SetMode(gin.TestMode)
	dialecttransport.ResetForTest()
	t.Cleanup(dialecttransport.ResetForTest)

	dialecttransport.RegisterInto(pingDialect("a-dialect", "pack-a", "/a"))
	dialecttransport.RegisterInto(pingDialect("b-dialect", "pack-b", "/b"))

	router := gin.New()
	mountRegistered(router, deps(), base(), testUserFromContext, permsKnown(),
		map[string]bool{"pack-a": true, "pack-b": false})

	if w := serve(router, "/a/v1/ping"); w.Code != http.StatusOK {
		t.Fatalf("pack-a active: GET /a/v1/ping = %d, want 200", w.Code)
	}
	if w := serve(router, "/b/v1/ping"); w.Code != http.StatusNotFound {
		t.Fatalf("pack-b inactive: GET /b/v1/ping = %d, want 404", w.Code)
	}
}

// TestMountRegisteredEmptyOwningPackAlwaysMounts: a partition with no pack dependency
// (OwningPack == "") mounts regardless of activePackNames, matching the binder's
// resolvePackActive contract (empty owner → always active/known).
func TestMountRegisteredEmptyOwningPackAlwaysMounts(t *testing.T) {
	gin.SetMode(gin.TestMode)
	dialecttransport.ResetForTest()
	t.Cleanup(dialecttransport.ResetForTest)

	dialecttransport.RegisterInto(pingDialect("packless", "", "/free"))

	router := gin.New()
	mountRegistered(router, deps(), base(), testUserFromContext, permsKnown(), map[string]bool{})

	if w := serve(router, "/free/v1/ping"); w.Code != http.StatusOK {
		t.Fatalf("packless dialect: GET /free/v1/ping = %d, want 200 (empty OwningPack always mounts)", w.Code)
	}
}
