package http

import (
	"bytes"
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/gin-gonic/gin"
	"go.uber.org/zap"
	"k8s.io/apimachinery/pkg/api/resource"
	"k8s.io/apimachinery/pkg/util/intstr"

	"github.com/sandbox0-ai/sandbox0/manager/pkg/apis/sandbox0/v1alpha1"
	"github.com/sandbox0-ai/sandbox0/pkg/internalauth"
	"github.com/sandbox0-ai/sandbox0/pkg/naming"
	"github.com/sandbox0-ai/sandbox0/pkg/template"
)

type testTemplateStore struct {
	getTemplateFn        func(ctx context.Context, scope, teamID, templateID string) (*template.Template, error)
	getTemplateForTeamFn func(ctx context.Context, teamID, templateID string) (*template.Template, error)
	listVisibleFn        func(ctx context.Context, teamID string) ([]*template.Template, error)
	createCalled         bool
	updateCalled         bool
	createdOrUpdatedID   string
	createdScope         string
	createdTeamID        string
	updatedScope         string
	updatedTeamID        string
}

func (s *testTemplateStore) CreateTemplate(_ context.Context, tpl *template.Template) error {
	s.createCalled = true
	s.createdOrUpdatedID = tpl.TemplateID
	s.createdScope = tpl.Scope
	s.createdTeamID = tpl.TeamID
	return nil
}

func (s *testTemplateStore) GetTemplate(ctx context.Context, scope, teamID, templateID string) (*template.Template, error) {
	if s.getTemplateFn != nil {
		return s.getTemplateFn(ctx, scope, teamID, templateID)
	}
	return nil, nil
}

func (s *testTemplateStore) GetTemplateForTeam(ctx context.Context, teamID, templateID string) (*template.Template, error) {
	if s.getTemplateForTeamFn != nil {
		return s.getTemplateForTeamFn(ctx, teamID, templateID)
	}
	return nil, nil
}

func (s *testTemplateStore) ListTemplates(context.Context) ([]*template.Template, error) {
	return nil, nil
}

func (s *testTemplateStore) ListVisibleTemplates(ctx context.Context, teamID string) ([]*template.Template, error) {
	if s.listVisibleFn != nil {
		return s.listVisibleFn(ctx, teamID)
	}
	return nil, nil
}

type testTemplateStatsProvider struct {
	stats *TemplateStats
	err   error
}

func (p *testTemplateStatsProvider) GetTemplateStats(context.Context) (*TemplateStats, error) {
	if p.err != nil {
		return nil, p.err
	}
	return p.stats, nil
}

func (s *testTemplateStore) UpdateTemplate(_ context.Context, tpl *template.Template) error {
	s.updateCalled = true
	s.createdOrUpdatedID = tpl.TemplateID
	s.updatedScope = tpl.Scope
	s.updatedTeamID = tpl.TeamID
	return nil
}

func (s *testTemplateStore) DeleteTemplate(context.Context, string, string, string) error {
	return nil
}

func TestCreateTemplate_RejectsPrivilegedFieldsForRegularTeam(t *testing.T) {
	t.Parallel()

	store := &testTemplateStore{}
	h := &Handler{Store: store, Logger: zap.NewNop()}

	router := gin.New()
	router.Use(withClaims(&internalauth.Claims{
		TeamID: "team-1",
		UserID: "user-1",
	}))
	router.POST("/api/v1/templates", h.CreateTemplate)

	body := []byte(`{
		"template_id":"demo",
		"spec":{
			"mainContainer":{"image":"ubuntu:22.04","resources":{"cpu":"1","memory":"4Gi"}},
			"pool":{"minIdle":0,"maxIdle":1},
			"pod":{"serviceAccountName":"custom-sa"}
		}
	}`)
	req := httptest.NewRequest(http.MethodPost, "/api/v1/templates", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusForbidden {
		t.Fatalf("expected status %d, got %d", http.StatusForbidden, rec.Code)
	}
	if store.createCalled {
		t.Fatalf("expected create not called for forbidden request")
	}
}

func TestCreateTemplate_RejectsImagePullPolicyForRegularTeam(t *testing.T) {
	t.Parallel()

	store := &testTemplateStore{}
	h := &Handler{Store: store, Logger: zap.NewNop()}

	router := gin.New()
	router.Use(withClaims(&internalauth.Claims{
		TeamID: "team-1",
		UserID: "user-1",
	}))
	router.POST("/api/v1/templates", h.CreateTemplate)

	body := []byte(`{
		"template_id":"demo",
		"spec":{
			"mainContainer":{
				"image":"ubuntu:22.04",
				"imagePullPolicy":"Always",
				"resources":{"cpu":"1","memory":"4Gi"}
			},
			"pool":{"minIdle":0,"maxIdle":1}
		}
	}`)
	req := httptest.NewRequest(http.MethodPost, "/api/v1/templates", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusForbidden {
		t.Fatalf("expected status %d, got %d", http.StatusForbidden, rec.Code)
	}
	if store.createCalled {
		t.Fatalf("expected create not called for forbidden request")
	}
}

func TestCreateTemplate_RejectsPrivateImageFromDifferentTeam(t *testing.T) {
	t.Parallel()

	store := &testTemplateStore{}
	h := &Handler{
		Store:                store,
		PrivateRegistryHosts: []string{"registry.internal.svc:5000"},
		Logger:               zap.NewNop(),
	}

	router := gin.New()
	router.Use(withClaims(&internalauth.Claims{
		TeamID: "team-1",
		UserID: "user-1",
	}))
	router.POST("/api/v1/templates", h.CreateTemplate)

	body := []byte(`{
		"template_id":"demo",
		"spec":{
			"mainContainer":{"image":"registry.internal.svc:5000/t-other/my-app:v1","resources":{"cpu":"1","memory":"4Gi"}},
			"pool":{"minIdle":0,"maxIdle":1}
		}
	}`)
	req := httptest.NewRequest(http.MethodPost, "/api/v1/templates", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusForbidden {
		t.Fatalf("expected status %d, got %d", http.StatusForbidden, rec.Code)
	}
	if store.createCalled {
		t.Fatalf("expected create not called for forbidden request")
	}
}

func TestCreateTemplate_AllowsTeamScopedPrivateImage(t *testing.T) {
	t.Parallel()

	store := &testTemplateStore{
		getTemplateFn: func(context.Context, string, string, string) (*template.Template, error) {
			return nil, nil
		},
	}
	h := &Handler{
		Store:                store,
		PrivateRegistryHosts: []string{"registry.internal.svc:5000"},
		Logger:               zap.NewNop(),
	}

	router := gin.New()
	router.Use(withClaims(&internalauth.Claims{
		TeamID: "team-1",
		UserID: "user-1",
	}))
	router.POST("/api/v1/templates", h.CreateTemplate)

	prefix := naming.TeamImageRepositoryPrefix("team-1")
	body := []byte(`{
		"template_id":"demo",
		"spec":{
			"mainContainer":{"image":"registry.internal.svc:5000/` + prefix + `/my-app:v1","resources":{"cpu":"1","memory":"4Gi"}},
			"pool":{"minIdle":0,"maxIdle":1}
		}
	}`)
	req := httptest.NewRequest(http.MethodPost, "/api/v1/templates", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusCreated {
		t.Fatalf("expected status %d, got %d", http.StatusCreated, rec.Code)
	}
	if !store.createCalled {
		t.Fatalf("expected create to be called")
	}
}

func TestCreateTemplate_AllowsNetworkForRegularTeam(t *testing.T) {
	t.Parallel()

	store := &testTemplateStore{
		getTemplateFn: func(context.Context, string, string, string) (*template.Template, error) {
			return nil, nil
		},
	}
	h := &Handler{Store: store, Logger: zap.NewNop()}

	router := gin.New()
	router.Use(withClaims(&internalauth.Claims{
		TeamID: "team-1",
		UserID: "user-1",
	}))
	router.POST("/api/v1/templates", h.CreateTemplate)

	body := []byte(`{
		"template_id":"demo",
		"spec":{
			"mainContainer":{"image":"ubuntu:22.04","resources":{"cpu":"1","memory":"4Gi"}},
			"pool":{"minIdle":0,"maxIdle":1},
			"network":{"mode":"block-all"}
		}
	}`)
	req := httptest.NewRequest(http.MethodPost, "/api/v1/templates", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusCreated {
		t.Fatalf("expected status %d, got %d", http.StatusCreated, rec.Code)
	}
	if !store.createCalled {
		t.Fatalf("expected create to be called")
	}
}

func TestCreateTemplate_AllowsPrivilegedFieldForSystemToken(t *testing.T) {
	t.Parallel()

	store := &testTemplateStore{
		getTemplateFn: func(context.Context, string, string, string) (*template.Template, error) {
			return nil, nil
		},
	}
	h := &Handler{Store: store, Logger: zap.NewNop()}

	router := gin.New()
	router.Use(withClaims(&internalauth.Claims{
		TeamID:   "team-1",
		UserID:   "system",
		IsSystem: true,
	}))
	router.POST("/api/v1/templates", h.CreateTemplate)

	body := []byte(`{
		"template_id":"demo",
		"spec":{
			"mainContainer":{
				"image":"ubuntu:22.04",
				"resources":{"cpu":"1","memory":"4Gi"},
				"securityContext":{"runAsUser":1000}
			},
			"pool":{"minIdle":0,"maxIdle":1}
		}
	}`)
	req := httptest.NewRequest(http.MethodPost, "/api/v1/templates", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusCreated {
		t.Fatalf("expected status %d, got %d", http.StatusCreated, rec.Code)
	}
	if !store.createCalled {
		t.Fatalf("expected create to be called")
	}
}

func TestCreateTemplate_SystemWithoutTeamCreatesPublicTemplate(t *testing.T) {
	t.Parallel()

	store := &testTemplateStore{
		getTemplateFn: func(_ context.Context, scope, teamID, templateID string) (*template.Template, error) {
			if scope != naming.ScopePublic || teamID != "" || templateID != "demo" {
				t.Fatalf("GetTemplate scope/team/id = %q/%q/%q, want public//demo", scope, teamID, templateID)
			}
			return nil, nil
		},
	}
	h := &Handler{Store: store, Logger: zap.NewNop()}

	router := gin.New()
	router.Use(withClaims(&internalauth.Claims{
		UserID:   "system",
		IsSystem: true,
	}))
	router.POST("/api/v1/templates", h.CreateTemplate)

	body := []byte(`{
		"template_id":"demo",
		"spec":{
			"mainContainer":{"image":"ubuntu:22.04","resources":{"cpu":"1","memory":"4Gi"}},
			"pool":{"minIdle":0,"maxIdle":1}
		}
	}`)
	req := httptest.NewRequest(http.MethodPost, "/api/v1/templates", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusCreated {
		t.Fatalf("expected status %d, got %d", http.StatusCreated, rec.Code)
	}
	if !store.createCalled {
		t.Fatalf("expected create to be called")
	}
	if store.createdScope != naming.ScopePublic || store.createdTeamID != "" {
		t.Fatalf("created scope/team = %q/%q, want public/empty", store.createdScope, store.createdTeamID)
	}
}

func TestGetTemplate_SystemWithoutTeamReadsPublicTemplate(t *testing.T) {
	t.Parallel()

	calledGetTemplate := false
	calledGetTemplateForTeam := false
	store := &testTemplateStore{
		getTemplateFn: func(_ context.Context, scope, teamID, templateID string) (*template.Template, error) {
			calledGetTemplate = true
			if scope != naming.ScopePublic || teamID != "" || templateID != "demo" {
				t.Fatalf("GetTemplate scope/team/id = %q/%q/%q, want public//demo", scope, teamID, templateID)
			}
			return &template.Template{TemplateID: templateID, Scope: scope, TeamID: teamID, Spec: validTemplateSpec()}, nil
		},
		getTemplateForTeamFn: func(context.Context, string, string) (*template.Template, error) {
			calledGetTemplateForTeam = true
			return nil, nil
		},
	}
	h := &Handler{Store: store, Logger: zap.NewNop()}

	router := gin.New()
	router.Use(withClaims(&internalauth.Claims{
		UserID:   "system",
		IsSystem: true,
	}))
	router.GET("/api/v1/templates/:id", h.GetTemplate)

	req := httptest.NewRequest(http.MethodGet, "/api/v1/templates/demo", nil)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("expected status %d, got %d", http.StatusOK, rec.Code)
	}
	if !calledGetTemplate {
		t.Fatalf("expected public GetTemplate to be called")
	}
	if calledGetTemplateForTeam {
		t.Fatalf("did not expect team fallback lookup for system public template")
	}
}

func TestCreateTemplate_RejectsMissingMainContainerImage(t *testing.T) {
	t.Parallel()

	store := &testTemplateStore{
		getTemplateFn: func(context.Context, string, string, string) (*template.Template, error) {
			return nil, nil
		},
	}
	h := &Handler{Store: store, Logger: zap.NewNop()}

	router := gin.New()
	router.Use(withClaims(&internalauth.Claims{
		TeamID: "team-1",
		UserID: "user-1",
	}))
	router.POST("/api/v1/templates", h.CreateTemplate)

	body := []byte(`{
		"template_id":"demo",
		"spec":{
			"mainContainer":{"resources":{"cpu":"1","memory":"4Gi"}},
			"pool":{"minIdle":0,"maxIdle":1}
		}
	}`)
	req := httptest.NewRequest(http.MethodPost, "/api/v1/templates", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusBadRequest {
		t.Fatalf("expected status %d, got %d", http.StatusBadRequest, rec.Code)
	}
	if store.createCalled {
		t.Fatalf("expected create not called for invalid request")
	}
}

func TestUpdateTemplate_RejectsInvalidPoolRange(t *testing.T) {
	t.Parallel()

	store := &testTemplateStore{
		getTemplateFn: func(context.Context, string, string, string) (*template.Template, error) {
			return &template.Template{
				TemplateID: "demo",
				Scope:      "team",
				TeamID:     "team-1",
				Spec: v1alpha1.SandboxTemplateSpec{
					MainContainer: v1alpha1.ContainerSpec{
						Image: "ubuntu:22.04",
						Resources: v1alpha1.ResourceQuota{
							CPU:    resource.MustParse("1"),
							Memory: resource.MustParse("4Gi"),
						},
					},
					Pool: v1alpha1.PoolStrategy{MinIdle: 0, MaxIdle: 1},
				},
			}, nil
		},
	}
	h := &Handler{Store: store, Logger: zap.NewNop()}

	router := gin.New()
	router.Use(withClaims(&internalauth.Claims{
		TeamID: "team-1",
		UserID: "user-1",
	}))
	router.PUT("/api/v1/templates/:id", h.UpdateTemplate)

	body := []byte(`{
		"spec":{
			"mainContainer":{"image":"ubuntu:22.04","resources":{"cpu":"1","memory":"4Gi"}},
			"pool":{"minIdle":2,"maxIdle":1}
		}
	}`)
	req := httptest.NewRequest(http.MethodPut, "/api/v1/templates/demo", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusBadRequest {
		t.Fatalf("expected status %d, got %d", http.StatusBadRequest, rec.Code)
	}
	if store.updateCalled {
		t.Fatalf("expected update not called for invalid request")
	}
}

func TestUpdateTemplate_AllowsWarmProcessesForRegularTeam(t *testing.T) {
	t.Parallel()

	store := &testTemplateStore{
		getTemplateFn: func(context.Context, string, string, string) (*template.Template, error) {
			return &template.Template{
				TemplateID: "demo",
				Scope:      "team",
				TeamID:     "team-1",
				Spec: v1alpha1.SandboxTemplateSpec{
					MainContainer: v1alpha1.ContainerSpec{
						Image: "ubuntu:22.04",
						Resources: v1alpha1.ResourceQuota{
							CPU:    resource.MustParse("1"),
							Memory: resource.MustParse("4Gi"),
						},
					},
					Pool: v1alpha1.PoolStrategy{MinIdle: 0, MaxIdle: 1},
				},
			}, nil
		},
	}
	h := &Handler{Store: store, Logger: zap.NewNop()}

	router := gin.New()
	router.Use(withClaims(&internalauth.Claims{
		TeamID: "team-1",
		UserID: "user-1",
	}))
	router.PUT("/api/v1/templates/:id", h.UpdateTemplate)

	body := []byte(`{
		"spec":{
			"mainContainer":{"image":"ubuntu:22.04","resources":{"cpu":"1","memory":"4Gi"}},
			"pool":{"minIdle":0,"maxIdle":1},
			"warmProcesses":[{"type":"cmd","command":["/bin/sh","-lc","sleep 3600"],"cwd":"/workspace","envVars":{"MODE":"warm"}}]
		}
	}`)
	req := httptest.NewRequest(http.MethodPut, "/api/v1/templates/demo", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("expected status %d, got %d", http.StatusOK, rec.Code)
	}
	if !store.updateCalled {
		t.Fatalf("expected update called for warm process request")
	}
}

func TestUpdateTemplate_SystemWithoutTeamUpdatesPublicTemplate(t *testing.T) {
	t.Parallel()

	store := &testTemplateStore{
		getTemplateFn: func(_ context.Context, scope, teamID, templateID string) (*template.Template, error) {
			if scope != naming.ScopePublic || teamID != "" || templateID != "demo" {
				t.Fatalf("GetTemplate scope/team/id = %q/%q/%q, want public//demo", scope, teamID, templateID)
			}
			return &template.Template{TemplateID: templateID, Scope: scope, TeamID: teamID, Spec: validTemplateSpec()}, nil
		},
	}
	h := &Handler{Store: store, Logger: zap.NewNop()}

	router := gin.New()
	router.Use(withClaims(&internalauth.Claims{
		UserID:   "system",
		IsSystem: true,
	}))
	router.PUT("/api/v1/templates/:id", h.UpdateTemplate)

	body := []byte(`{
		"spec":{
			"mainContainer":{"image":"ubuntu:22.04","resources":{"cpu":"1","memory":"4Gi"}},
			"pool":{"minIdle":0,"maxIdle":1}
		}
	}`)
	req := httptest.NewRequest(http.MethodPut, "/api/v1/templates/demo", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("expected status %d, got %d", http.StatusOK, rec.Code)
	}
	if !store.updateCalled {
		t.Fatalf("expected update to be called")
	}
	if store.updatedScope != naming.ScopePublic || store.updatedTeamID != "" {
		t.Fatalf("updated scope/team = %q/%q, want public/empty", store.updatedScope, store.updatedTeamID)
	}
}

func TestCreateTemplate_AllowsWarmProcessesForRegularTeam(t *testing.T) {
	t.Parallel()

	store := &testTemplateStore{}
	h := &Handler{Store: store, Logger: zap.NewNop()}

	router := gin.New()
	router.Use(withClaims(&internalauth.Claims{
		TeamID: "team-1",
		UserID: "user-1",
	}))
	router.POST("/api/v1/templates", h.CreateTemplate)

	body := []byte(`{
		"template_id":"demo",
		"spec":{
			"mainContainer":{"image":"ubuntu:22.04","resources":{"cpu":"1","memory":"4Gi"}},
			"pool":{"minIdle":0,"maxIdle":1},
			"warmProcesses":[{"type":"repl","alias":"bash","cwd":"/workspace"}]
		}
	}`)
	req := httptest.NewRequest(http.MethodPost, "/api/v1/templates", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusCreated {
		t.Fatalf("expected status %d, got %d", http.StatusCreated, rec.Code)
	}
	if !store.createCalled {
		t.Fatalf("expected create called for warm process request")
	}
}

func TestUpdateTemplate_RejectsImagePullPolicyForRegularTeam(t *testing.T) {
	t.Parallel()

	store := &testTemplateStore{
		getTemplateFn: func(context.Context, string, string, string) (*template.Template, error) {
			return &template.Template{
				TemplateID: "demo",
				Scope:      "team",
				TeamID:     "team-1",
				Spec: v1alpha1.SandboxTemplateSpec{
					MainContainer: v1alpha1.ContainerSpec{
						Image: "ubuntu:22.04",
						Resources: v1alpha1.ResourceQuota{
							CPU:    resource.MustParse("1"),
							Memory: resource.MustParse("4Gi"),
						},
					},
					Pool: v1alpha1.PoolStrategy{MinIdle: 0, MaxIdle: 1},
				},
			}, nil
		},
	}
	h := &Handler{Store: store, Logger: zap.NewNop()}

	router := gin.New()
	router.Use(withClaims(&internalauth.Claims{
		TeamID: "team-1",
		UserID: "user-1",
	}))
	router.PUT("/api/v1/templates/:id", h.UpdateTemplate)

	body := []byte(`{
		"spec":{
			"mainContainer":{
				"image":"ubuntu:22.04",
				"imagePullPolicy":"Always",
				"resources":{"cpu":"1","memory":"4Gi"}
			},
			"pool":{"minIdle":0,"maxIdle":1}
		}
	}`)
	req := httptest.NewRequest(http.MethodPut, "/api/v1/templates/demo", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusForbidden {
		t.Fatalf("expected status %d, got %d", http.StatusForbidden, rec.Code)
	}
	if store.updateCalled {
		t.Fatalf("expected update not called for forbidden request")
	}
}

func TestCreateTemplate_RejectsUnclaimableNameBudget(t *testing.T) {
	t.Parallel()

	store := &testTemplateStore{}
	h := &Handler{Store: store, Logger: zap.NewNop()}

	router := gin.New()
	router.Use(withClaims(&internalauth.Claims{IsSystem: true}))
	router.POST("/api/v1/templates", h.CreateTemplate)

	clusterID := strings.Repeat("a", naming.ClusterIDMaxLen+1)
	body := []byte(`{
		"template_id":"demo",
		"spec":{
			"clusterId":"` + clusterID + `",
			"mainContainer":{"image":"ubuntu:22.04","resources":{"cpu":"1","memory":"4Gi"}},
			"pool":{"minIdle":0,"maxIdle":1}
		}
	}`)
	req := httptest.NewRequest(http.MethodPost, "/api/v1/templates", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusBadRequest {
		t.Fatalf("expected status %d, got %d: %s", http.StatusBadRequest, rec.Code, rec.Body.String())
	}
	if !strings.Contains(rec.Body.String(), "cannot generate claimable sandbox names") {
		t.Fatalf("expected claimable name budget error, got %s", rec.Body.String())
	}
	if store.createCalled {
		t.Fatalf("expected create not called for unclaimable name budget")
	}
}

func TestValidateTemplateSpecForClaims_WildcardPermissionRejected(t *testing.T) {
	t.Parallel()

	spec := v1alpha1.SandboxTemplateSpec{
		MainContainer: v1alpha1.ContainerSpec{
			Image: "ubuntu:22.04",
			Resources: v1alpha1.ResourceQuota{
				CPU:    resource.MustParse("1"),
				Memory: resource.MustParse("4Gi"),
			},
			SecurityContext: &v1alpha1.SecurityContext{
				RunAsUser: ptrInt64(1000),
			},
		},
		Pool: v1alpha1.PoolStrategy{MinIdle: 0, MaxIdle: 1},
	}

	err := validateTemplateSpecForClaims(spec, &internalauth.Claims{
		Permissions: []string{"*"},
	})
	if err == nil {
		t.Fatalf("expected wildcard permission to be rejected")
	}
}

func TestValidateTemplateClaimNameBudget_AllowsMaxLengthTemplateID(t *testing.T) {
	t.Parallel()

	err := validateTemplateClaimNameBudget(naming.ScopeTeam, "team-1", strings.Repeat("a", 255), validTemplateSpec())
	if err != nil {
		t.Fatalf("validateTemplateClaimNameBudget: %v", err)
	}
}

func TestValidateTemplateClaimNameBudget_RejectsUnclaimableClusterID(t *testing.T) {
	t.Parallel()

	spec := validTemplateSpec()
	clusterID := strings.Repeat("a", naming.ClusterIDMaxLen+1)
	spec.ClusterId = &clusterID
	err := validateTemplateClaimNameBudget(naming.ScopePublic, "", "demo", spec)
	if err == nil {
		t.Fatalf("expected unclaimable naming budget error")
	}
	if !strings.Contains(err.Error(), "cannot generate claimable sandbox names") {
		t.Fatalf("unexpected error: %v", err)
	}
}

func TestValidateTemplateSpec_StrictValidation(t *testing.T) {
	t.Parallel()

	newSpec := func() v1alpha1.SandboxTemplateSpec {
		return v1alpha1.SandboxTemplateSpec{
			MainContainer: v1alpha1.ContainerSpec{
				Image: "ubuntu:22.04",
				Resources: v1alpha1.ResourceQuota{
					CPU:    resource.MustParse("1"),
					Memory: resource.MustParse("4Gi"),
				},
			},
			Pool: v1alpha1.PoolStrategy{MinIdle: 0, MaxIdle: 1},
		}
	}

	cases := []struct {
		name    string
		mutate  func(*v1alpha1.SandboxTemplateSpec)
		wantErr string
	}{
		{
			name: "reject missing cpu",
			mutate: func(s *v1alpha1.SandboxTemplateSpec) {
				s.MainContainer.Resources.CPU = resource.MustParse("0")
			},
			wantErr: "spec.mainContainer.resources.cpu must be > 0",
		},
		{
			name: "reject invalid network mode",
			mutate: func(s *v1alpha1.SandboxTemplateSpec) {
				s.Network = &v1alpha1.SandboxNetworkPolicy{
					Mode: "deny-all",
				}
			},
			wantErr: "spec.network.mode must be one of: allow-all, block-all",
		},
		{
			name: "reject invalid cidr",
			mutate: func(s *v1alpha1.SandboxTemplateSpec) {
				s.Network = &v1alpha1.SandboxNetworkPolicy{
					Mode: v1alpha1.NetworkModeBlockAll,
					Egress: &v1alpha1.NetworkEgressPolicy{
						AllowedCIDRs: []string{"not-a-cidr"},
					},
				}
			},
			wantErr: "spec.network.egress.allowedCidrs[0] must be valid CIDR",
		},
		{
			name: "reject invalid port range",
			mutate: func(s *v1alpha1.SandboxTemplateSpec) {
				s.Network = &v1alpha1.SandboxNetworkPolicy{
					Mode: v1alpha1.NetworkModeBlockAll,
					Egress: &v1alpha1.NetworkEgressPolicy{
						AllowedPorts: []v1alpha1.PortSpec{
							{Port: 1000, EndPort: ptrInt32(999)},
						},
					},
				}
			},
			wantErr: "spec.network.egress.allowedPorts[0].endPort must be between port and 65535",
		},
		{
			name: "reject cmd warm process without command",
			mutate: func(s *v1alpha1.SandboxTemplateSpec) {
				s.WarmProcesses = []v1alpha1.WarmProcessSpec{{Type: v1alpha1.WarmProcessTypeCMD}}
			},
			wantErr: "spec.warmProcesses[0].command is required for cmd warm processes",
		},
		{
			name: "reject repl warm process with command",
			mutate: func(s *v1alpha1.SandboxTemplateSpec) {
				s.WarmProcesses = []v1alpha1.WarmProcessSpec{{Type: v1alpha1.WarmProcessTypeREPL, Command: []string{"bash"}}}
			},
			wantErr: "spec.warmProcesses[0].command is only valid for cmd warm processes",
		},
		{
			name: "reject invalid warm process type",
			mutate: func(s *v1alpha1.SandboxTemplateSpec) {
				s.WarmProcesses = []v1alpha1.WarmProcessSpec{{Type: "daemon"}}
			},
			wantErr: "spec.warmProcesses[0].type must be one of: repl, cmd",
		},
		{
			name: "reject warm process probe without handler",
			mutate: func(s *v1alpha1.SandboxTemplateSpec) {
				s.WarmProcesses = []v1alpha1.WarmProcessSpec{{
					Type:   v1alpha1.WarmProcessTypeREPL,
					Probes: &v1alpha1.SandboxProbeSet{Readiness: &v1alpha1.SandboxProbeSpec{}},
				}}
			},
			wantErr: "spec.warmProcesses[0].probes.readiness must configure one of process, exec, httpGet, or tcpSocket",
		},
		{
			name: "reject warm process probe with multiple handlers",
			mutate: func(s *v1alpha1.SandboxTemplateSpec) {
				s.WarmProcesses = []v1alpha1.WarmProcessSpec{{
					Type: v1alpha1.WarmProcessTypeREPL,
					Probes: &v1alpha1.SandboxProbeSet{Liveness: &v1alpha1.SandboxProbeSpec{
						Process: &v1alpha1.ProcessProbeSpec{},
						Exec:    &v1alpha1.ExecProbeSpec{Command: []string{"true"}},
					}},
				}}
			},
			wantErr: "spec.warmProcesses[0].probes.liveness must configure only one of process, exec, httpGet, or tcpSocket",
		},
		{
			name: "reject warm process probe with invalid http port",
			mutate: func(s *v1alpha1.SandboxTemplateSpec) {
				s.WarmProcesses = []v1alpha1.WarmProcessSpec{{
					Type: v1alpha1.WarmProcessTypeREPL,
					Probes: &v1alpha1.SandboxProbeSet{Startup: &v1alpha1.SandboxProbeSpec{
						HTTPGet: &v1alpha1.HTTPGetProbeSpec{Port: intstr.FromInt(0)},
					}},
				}}
			},
			wantErr: "spec.warmProcesses[0].probes.startup.httpGet.port must be positive",
		},
	}

	for _, tc := range cases {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			spec := newSpec()
			tc.mutate(&spec)

			err := validateTemplateSpec(spec)
			if err == nil {
				t.Fatalf("expected error, got nil")
			}
			if got := err.Error(); got != tc.wantErr && !bytes.Contains([]byte(got), []byte(tc.wantErr)) {
				t.Fatalf("expected error containing %q, got %q", tc.wantErr, got)
			}
		})
	}
}

func TestValidateTemplateSpecForClaims_AllowsWarmProcesses(t *testing.T) {
	t.Parallel()

	spec := v1alpha1.SandboxTemplateSpec{
		MainContainer: v1alpha1.ContainerSpec{
			Image: "ubuntu:22.04",
			Resources: v1alpha1.ResourceQuota{
				CPU:    resource.MustParse("1"),
				Memory: resource.MustParse("4Gi"),
			},
		},
		WarmProcesses: []v1alpha1.WarmProcessSpec{{
			Type:  v1alpha1.WarmProcessTypeREPL,
			Alias: "bash",
			Probes: &v1alpha1.SandboxProbeSet{Readiness: &v1alpha1.SandboxProbeSpec{
				Process: &v1alpha1.ProcessProbeSpec{},
			}},
		}},
		Pool: v1alpha1.PoolStrategy{MinIdle: 0, MaxIdle: 1},
	}

	if err := validateTemplateSpecForClaims(spec, &internalauth.Claims{TeamID: "team-1"}); err != nil {
		t.Fatalf("expected warm processes to be allowed, got %v", err)
	}
}

func TestValidateTemplateSpecForClaims_RejectsMismatchedMainResources(t *testing.T) {
	t.Parallel()

	spec := v1alpha1.SandboxTemplateSpec{
		MainContainer: v1alpha1.ContainerSpec{
			Image: "ubuntu:22.04",
			Resources: v1alpha1.ResourceQuota{
				CPU:    resource.MustParse("1"),
				Memory: resource.MustParse("1Gi"),
			},
		},
		Pool: v1alpha1.PoolStrategy{MinIdle: 0, MaxIdle: 1},
	}

	err := validateTemplateSpecForClaims(spec, &internalauth.Claims{TeamID: "team-1"})
	if err == nil {
		t.Fatal("expected aggregate resource ratio to be rejected")
	}
	if got := err.Error(); !strings.Contains(got, "team-owned template total memory must equal total cpu") {
		t.Fatalf("unexpected error %q", got)
	}
}

func TestValidateTemplateSpecForClaims_AllowsSystemOwnedRatioOverride(t *testing.T) {
	t.Parallel()

	spec := v1alpha1.SandboxTemplateSpec{
		MainContainer: v1alpha1.ContainerSpec{
			Image: "ubuntu:22.04",
			Resources: v1alpha1.ResourceQuota{
				CPU:    resource.MustParse("1"),
				Memory: resource.MustParse("1Gi"),
			},
		},
		Pool: v1alpha1.PoolStrategy{MinIdle: 0, MaxIdle: 1},
	}

	claims := &internalauth.Claims{IsSystem: true}
	if err := validateTemplateSpecForClaims(spec, claims); err != nil {
		t.Fatalf("expected system token to allow resource ratio override, got %v", err)
	}
}

func TestGetTemplate_IncludesPoolStatus(t *testing.T) {
	t.Parallel()

	namespace, err := naming.TemplateNamespaceForTeam("team-1")
	if err != nil {
		t.Fatalf("resolve team namespace: %v", err)
	}

	store := &testTemplateStore{
		getTemplateForTeamFn: func(context.Context, string, string) (*template.Template, error) {
			return &template.Template{
				TemplateID: "demo",
				Scope:      "team",
				TeamID:     "team-1",
			}, nil
		},
	}
	statusProvider := &testTemplateStatsProvider{
		stats: &TemplateStats{
			Templates: []TemplateStat{
				{
					TemplateID:  naming.TemplateNameForCluster("team", "team-1", "demo"),
					Namespace:   namespace,
					IdleCount:   3,
					ActiveCount: 7,
				},
			},
		},
	}
	h := &Handler{Store: store, StatsProvider: statusProvider, Logger: zap.NewNop()}

	router := gin.New()
	router.Use(withClaims(&internalauth.Claims{
		TeamID: "team-1",
		UserID: "user-1",
	}))
	router.GET("/api/v1/templates/:id", h.GetTemplate)

	req := httptest.NewRequest(http.MethodGet, "/api/v1/templates/demo", nil)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("expected status %d, got %d", http.StatusOK, rec.Code)
	}

	var resp struct {
		Success bool `json:"success"`
		Data    struct {
			Status struct {
				IdleCount   int32 `json:"idleCount"`
				ActiveCount int32 `json:"activeCount"`
			} `json:"status"`
		} `json:"data"`
	}
	if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
		t.Fatalf("unmarshal response: %v", err)
	}
	if !resp.Success {
		t.Fatalf("expected success response")
	}
	if resp.Data.Status.IdleCount != 3 || resp.Data.Status.ActiveCount != 7 {
		t.Fatalf("unexpected status payload: idle=%d active=%d", resp.Data.Status.IdleCount, resp.Data.Status.ActiveCount)
	}
}

func withClaims(claims *internalauth.Claims) gin.HandlerFunc {
	return func(c *gin.Context) {
		ctx := internalauth.WithClaims(c.Request.Context(), claims)
		c.Request = c.Request.WithContext(ctx)
		c.Next()
	}
}

func validTemplateSpec() v1alpha1.SandboxTemplateSpec {
	return v1alpha1.SandboxTemplateSpec{
		MainContainer: v1alpha1.ContainerSpec{
			Image: "ubuntu:22.04",
			Resources: v1alpha1.ResourceQuota{
				CPU:    resource.MustParse("1"),
				Memory: resource.MustParse("4Gi"),
			},
		},
		Pool: v1alpha1.PoolStrategy{MinIdle: 0, MaxIdle: 1},
	}
}

func ptrInt64(v int64) *int64 {
	return &v
}

func ptrInt32(v int32) *int32 {
	return &v
}
