package dialecttransport_test

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

	"github.com/gin-gonic/gin"

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

var allCredentialPolicies = []dialect.CredentialPolicy{
	dialect.CredentialProgrammaticOnly,
	dialect.CredentialBrowserOrProgrammatic,
	dialect.CredentialBrowserOnly,
	dialect.CredentialBrowserEstablishment,
	dialect.CredentialBrowserTermination,
}

// TestCredentialPolicyWideningIsFailClosed pins the predicate a composition step
// uses to refuse a route override that broadens admission.
//
// The expectation is the complete 5x5 matrix rather than a few sampled pairs,
// because the earlier version of this predicate missed exactly the pairs nobody
// had enumerated: establishment to termination, and dual to establishment.
func TestCredentialPolicyWideningIsFailClosed(t *testing.T) {
	const (
		po  = iota // programmatic_only
		bop        // browser_or_programmatic
		bo         // browser_only
		be         // browser_establishment
		bt         // browser_termination
	)
	order := []dialect.CredentialPolicy{
		po:  dialect.CredentialProgrammaticOnly,
		bop: dialect.CredentialBrowserOrProgrammatic,
		bo:  dialect.CredentialBrowserOnly,
		be:  dialect.CredentialBrowserEstablishment,
		bt:  dialect.CredentialBrowserTermination,
	}

	// want[replacement][base] — does replacing base with replacement widen?
	// Derived from the admission dimensions: programmatic, established session,
	// unauthenticated first-party.
	want := [5][5]bool{
		//              base: po     bop    bo     be     bt
		po:  {false, false, true, true, true},
		bop: {true, false, true, true, true},
		bo:  {true, false, false, true, false},
		be:  {true, true, true, false, false},
		bt:  {true, true, true, true, false},
	}

	for ri, replacement := range order {
		for bi, base := range order {
			got := replacement.Widens(base)
			if got != want[ri][bi] {
				t.Fatalf("%s replacing %s: Widens = %v, want %v", replacement, base, got, want[ri][bi])
			}
		}
	}

	// Spot-check the two cases the earlier predicate got wrong, named explicitly so
	// a regression is legible rather than a matrix cell.
	if !dialect.CredentialBrowserTermination.Widens(dialect.CredentialBrowserEstablishment) {
		t.Fatal("termination admits an established session that establishment does not; must widen it")
	}
	if !dialect.CredentialBrowserEstablishment.Widens(dialect.CredentialBrowserOrProgrammatic) {
		t.Fatal("establishment admits an unauthenticated first-party request that the dual policy does not; must widen it")
	}

	// The retained deprecated predicate keeps its original meaning — true only for
	// browser_only — and is deliberately not part of the widening rule, since
	// treating it as sufficient is what hid the two cases above.
	for _, policy := range order {
		want := policy == dialect.CredentialBrowserOnly
		if got := policy.RequiresEstablishedBrowserSession(); got != want {
			t.Fatalf("%s RequiresEstablishedBrowserSession = %v, want %v", policy, got, want)
		}
	}

	// No policy widens itself, and narrowing a dual route to either single lane is
	// permitted.
	for _, policy := range order {
		if policy.Widens(policy) {
			t.Fatalf("%s must not widen itself", policy)
		}
	}

	// Policies that each admit a class the other rejects widen in both directions,
	// so neither may replace the other.
	if !dialect.CredentialProgrammaticOnly.Widens(dialect.CredentialBrowserOnly) ||
		!dialect.CredentialBrowserOnly.Widens(dialect.CredentialProgrammaticOnly) {
		t.Fatal("incomparable policies must widen in both directions")
	}
}

// TestMountResolvesEveryPolicyExactlyOncePerRoute covers the mount seam: each
// route consults the declaration once and runs the stack chosen for it, in the
// documented order.
func TestMountResolvesEveryPolicyExactlyOncePerRoute(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()

	d, resolver := policyDialect(allCredentialPolicies...)
	resolverCalls := map[string]int{}
	countingResolver := func(dialectName string, route dialect.Route) dialect.CredentialPolicy {
		resolverCalls[route.Path]++
		return resolver(dialectName, route)
	}

	var order []string
	provider := func(policy dialect.CredentialPolicy) []gin.HandlerFunc {
		return []gin.HandlerFunc{func(c *gin.Context) {
			order = append(order, "stack:"+policy.String())
			c.Next()
		}}
	}

	dialecttransport.Mount(router, []dialect.Dialect{d}, deps(), base(),
		testUserFromContext, permsKnown(), map[string]bool{},
		dialecttransport.WithCredentialStackProvider(provider),
		dialecttransport.WithRouteCredentialPolicyResolver(countingResolver))

	for index, policy := range allCredentialPolicies {
		routePath := "/svc/v1/r" + string(rune('0'+index))
		order = nil
		if w := serve(router, routePath); w.Code != http.StatusOK {
			t.Fatalf("%s = %d, want 200", routePath, w.Code)
		}
		if len(order) != 1 || order[0] != "stack:"+policy.String() {
			t.Fatalf("%s ran %v, want the %s stack only", routePath, order, policy)
		}
	}

	// The declaration is consulted at mount, once per route, not per request.
	for index := range allCredentialPolicies {
		key := "/r" + string(rune('0'+index))
		if resolverCalls[key] != 1 {
			t.Fatalf("resolver called %d times for %s, want exactly 1", resolverCalls[key], key)
		}
	}
}

// TestMountRunsPolicyStackBeforeGateAndHandler pins the composition order, since
// a gate or handler that ran before admission would observe an unauthenticated
// request.
func TestMountRunsPolicyStackBeforeGateAndHandler(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	var order []string

	d := testDialect{
		name: "order-dialect", pack: "", base: "/svc", vers: []string{"v1"},
		proj: notFoundProjector,
		manifest: []dialect.Route{{
			Method: http.MethodGet,
			Path:   "/gated",
			Gate:   dialect.Gate{Mode: dialect.GateAll, Permissions: []dialect.Permission{testPermViewSetup}},
			Handler: func(_ context.Context, _ *dialect.Request, _ dialect.Deps) (*dialect.Response, error) {
				order = append(order, "handler")
				return &dialect.Response{Status: http.StatusOK, Body: gin.H{"ok": true}}, nil
			},
		}},
	}

	dialecttransport.Mount(router, []dialect.Dialect{d}, deps(), base(),
		testUserFromContext, permsKnown(testPermViewSetup), map[string]bool{},
		dialecttransport.WithPermissionGateProvider(recordingGateProvider{order: &order}),
		dialecttransport.WithCredentialStackProvider(func(policy dialect.CredentialPolicy) []gin.HandlerFunc {
			return []gin.HandlerFunc{func(c *gin.Context) {
				order = append(order, "stack")
				c.Next()
			}}
		}),
		dialecttransport.WithRouteCredentialPolicyResolver(
			func(string, dialect.Route) dialect.CredentialPolicy {
				return dialect.CredentialBrowserOrProgrammatic
			}))

	if w := serve(router, "/svc/v1/gated"); w.Code != http.StatusOK {
		t.Fatalf("gated route = %d, want 200", w.Code)
	}
	want := []string{"stack", "gate", "handler"}
	if len(order) != len(want) {
		t.Fatalf("order = %v, want %v", order, want)
	}
	for i := range want {
		if order[i] != want[i] {
			t.Fatalf("order = %v, want %v", order, want)
		}
	}
}

// recordingGateProvider records that the declarative gate ran, so the ordering
// test can assert admission precedes it.
type recordingGateProvider struct{ order *[]string }

func (p recordingGateProvider) record() gin.HandlerFunc {
	return func(c *gin.Context) {
		*p.order = append(*p.order, "gate")
		c.Next()
	}
}

func (p recordingGateProvider) RequireAll([]dialect.Permission) []gin.HandlerFunc {
	return []gin.HandlerFunc{p.record()}
}

func (p recordingGateProvider) RequireAny([]dialect.Permission) gin.HandlerFunc {
	return p.record()
}

// TestInternalDispatchDoesNotInheritAmbientAuthority covers the dispatch seam: an
// internal subrequest re-enters the router, so the target route's own declared
// admission runs and the caller's admission does not leak into it.
func TestInternalDispatchDoesNotInheritAmbientAuthority(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	var ran []string

	dispatchDialect := testDialect{
		name: "dispatch-dialect", pack: "", base: "/svc", vers: []string{"v1"},
		proj: notFoundProjector,
		manifest: []dialect.Route{
			{
				// Declared browser-capable; dispatches into the raw route below.
				Method: http.MethodGet,
				Path:   "/outer",
				Gate:   dialect.Gate{Mode: dialect.GateNone},
				Handler: func(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
					resp, err := deps.Dispatch(ctx, dialect.DispatchRequest{
						Method: http.MethodGet,
						Path:   "/svc/v1/inner",
					})
					if err != nil {
						return nil, err
					}
					return &dialect.Response{Status: resp.Status, Body: gin.H{"inner": string(resp.Body)}}, nil
				},
			},
			{
				// Declared programmatic-only: its own stack must run on dispatch.
				Method: http.MethodGet,
				Path:   "/inner",
				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
				},
			},
		},
	}

	dialecttransport.Mount(router, []dialect.Dialect{dispatchDialect}, deps(), base(),
		testUserFromContext, permsKnown(), map[string]bool{},
		dialecttransport.WithCredentialStackProvider(func(policy dialect.CredentialPolicy) []gin.HandlerFunc {
			return []gin.HandlerFunc{func(c *gin.Context) {
				ran = append(ran, policy.String())
				c.Next()
			}}
		}),
		dialecttransport.WithRouteCredentialPolicyResolver(
			func(_ string, route dialect.Route) dialect.CredentialPolicy {
				if route.Path == "/outer" {
					return dialect.CredentialBrowserOrProgrammatic
				}
				return dialect.CredentialProgrammaticOnly
			}))

	if w := serve(router, "/svc/v1/outer"); w.Code != http.StatusOK {
		t.Fatalf("outer route = %d, want 200", w.Code)
	}

	// The browser-capable outer stack ran, then the dispatched subrequest ran the
	// inner route's own programmatic-only stack rather than continuing under the
	// caller's admission.
	want := []string{"browser_or_programmatic", "programmatic_only"}
	if len(ran) != len(want) {
		t.Fatalf("stacks ran %v, want %v", ran, want)
	}
	for i := range want {
		if ran[i] != want[i] {
			t.Fatalf("stacks ran %v, want %v", ran, want)
		}
	}
}

// TestRegisterAuthenticatedRouteComposesAdmissionBeforeHandlers covers the
// direct-registration seam, including under a router group.
func TestRegisterAuthenticatedRouteComposesAdmissionBeforeHandlers(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	var order []string

	provider := func(policy dialect.CredentialPolicy) []gin.HandlerFunc {
		return []gin.HandlerFunc{func(c *gin.Context) {
			order = append(order, "stack:"+policy.String())
			c.Next()
		}}
	}

	group := router.Group("/host")
	dialecttransport.RegisterAuthenticatedRoute(group, provider,
		dialect.CredentialBrowserTermination, http.MethodPost, "/logout",
		func(c *gin.Context) {
			order = append(order, "first")
			c.Next()
		},
		func(c *gin.Context) {
			order = append(order, "second")
			c.Status(http.StatusNoContent)
		})

	recorder := httptest.NewRecorder()
	router.ServeHTTP(recorder, httptest.NewRequest(http.MethodPost, "/host/logout", nil))
	if recorder.Code != http.StatusNoContent {
		t.Fatalf("host route = %d, want 204", recorder.Code)
	}

	want := []string{"stack:browser_termination", "first", "second"}
	if len(order) != len(want) {
		t.Fatalf("order = %v, want %v", order, want)
	}
	for i := range want {
		if order[i] != want[i] {
			t.Fatalf("order = %v, want %v", order, want)
		}
	}
}

// TestRegisterAuthenticatedRouteAdmitsEveryPolicy proves no declared policy is
// silently unroutable through the host registrar.
func TestRegisterAuthenticatedRouteAdmitsEveryPolicy(t *testing.T) {
	gin.SetMode(gin.TestMode)
	for _, policy := range allCredentialPolicies {
		router := gin.New()
		var selected dialect.CredentialPolicy
		dialecttransport.RegisterAuthenticatedRoute(router,
			func(p dialect.CredentialPolicy) []gin.HandlerFunc {
				selected = p
				return []gin.HandlerFunc{func(c *gin.Context) { c.Next() }}
			},
			policy, http.MethodGet, "/host/probe",
			func(c *gin.Context) { c.Status(http.StatusOK) })

		recorder := httptest.NewRecorder()
		router.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/host/probe", nil))
		if recorder.Code != http.StatusOK {
			t.Fatalf("%s route = %d, want 200", policy, recorder.Code)
		}
		if selected != policy {
			t.Fatalf("provider asked for %s, want %s", selected, policy)
		}
	}
}
