package http

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

	"github.com/sandbox0-ai/sandbox0/pkg/ctldapi"
	"github.com/sandbox0-ai/sandbox0/pkg/gateway/spec"
	"github.com/sandbox0-ai/sandbox0/pkg/internalauth"
	"github.com/sandbox0-ai/sandbox0/storage-proxy/pkg/db"
	"github.com/sandbox0-ai/sandbox0/storage-proxy/pkg/snapshot"
	"github.com/sandbox0-ai/sandbox0/storage-proxy/pkg/volume"
	"github.com/sirupsen/logrus"
)

type captureForkSnapshotManager struct {
	*fakeHTTPSnapshotManager
	lastFork *snapshot.ForkVolumeRequest
	forkResp *db.SandboxVolume
	forkErr  error
}

func (m *captureForkSnapshotManager) ForkVolume(_ context.Context, req *snapshot.ForkVolumeRequest) (*db.SandboxVolume, error) {
	m.lastFork = req
	if m.forkErr != nil {
		return nil, m.forkErr
	}
	if m.forkResp != nil {
		return m.forkResp, nil
	}
	return &db.SandboxVolume{ID: "fork-1", TeamID: req.TeamID, UserID: req.UserID}, nil
}

func TestCreateSandboxVolumeStoresDefaultPosixIdentity(t *testing.T) {
	repo := newFakeHTTPRepo()
	server := &Server{
		logger:       logrus.New(),
		repo:         repo,
		meteringRepo: &fakeHTTPMeteringWriter{},
		snapshotMgr:  &fakeHTTPSnapshotManager{},
	}

	uid := int64(1001)
	gid := int64(2002)
	req := httptest.NewRequest(http.MethodPost, "/sandboxvolumes", bytes.NewReader([]byte(`{"default_posix_uid":1001,"default_posix_gid":2002}`)))
	req = req.WithContext(internalauth.WithClaims(req.Context(), &internalauth.Claims{TeamID: "team-1", UserID: "user-1"}))
	recorder := httptest.NewRecorder()

	server.createSandboxVolume(recorder, req)

	if recorder.Code != http.StatusCreated {
		t.Fatalf("status = %d, want %d", recorder.Code, http.StatusCreated)
	}
	if len(repo.createdVolumes) != 1 {
		t.Fatalf("created volumes = %d, want 1", len(repo.createdVolumes))
	}
	created := repo.createdVolumes[0]
	if created.DefaultPosixUID == nil || *created.DefaultPosixUID != uid {
		t.Fatalf("DefaultPosixUID = %v, want %d", created.DefaultPosixUID, uid)
	}
	if created.DefaultPosixGID == nil || *created.DefaultPosixGID != gid {
		t.Fatalf("DefaultPosixGID = %v, want %d", created.DefaultPosixGID, gid)
	}
	resp, apiErr, err := spec.DecodeResponse[db.SandboxVolume](recorder.Body)
	if err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if apiErr != nil {
		t.Fatalf("unexpected api error: %+v", apiErr)
	}
	if resp.DefaultPosixUID == nil || *resp.DefaultPosixUID != uid {
		t.Fatalf("response DefaultPosixUID = %v, want %d", resp.DefaultPosixUID, uid)
	}
	if resp.DefaultPosixGID == nil || *resp.DefaultPosixGID != gid {
		t.Fatalf("response DefaultPosixGID = %v, want %d", resp.DefaultPosixGID, gid)
	}
}

func TestCreateSandboxVolumeDefaultsPosixIdentityToRoot(t *testing.T) {
	repo := newFakeHTTPRepo()
	server := &Server{
		logger:       logrus.New(),
		repo:         repo,
		meteringRepo: &fakeHTTPMeteringWriter{},
		snapshotMgr:  &fakeHTTPSnapshotManager{},
	}

	req := httptest.NewRequest(http.MethodPost, "/sandboxvolumes", bytes.NewReader([]byte(`{}`)))
	req = req.WithContext(internalauth.WithClaims(req.Context(), &internalauth.Claims{TeamID: "team-1", UserID: "user-1"}))
	recorder := httptest.NewRecorder()

	server.createSandboxVolume(recorder, req)

	if recorder.Code != http.StatusCreated {
		t.Fatalf("status = %d, want %d", recorder.Code, http.StatusCreated)
	}
	if len(repo.createdVolumes) != 1 {
		t.Fatalf("created volumes = %d, want 1", len(repo.createdVolumes))
	}
	created := repo.createdVolumes[0]
	if created.DefaultPosixUID == nil || *created.DefaultPosixUID != 0 {
		t.Fatalf("DefaultPosixUID = %v, want 0", created.DefaultPosixUID)
	}
	if created.DefaultPosixGID == nil || *created.DefaultPosixGID != 0 {
		t.Fatalf("DefaultPosixGID = %v, want 0", created.DefaultPosixGID)
	}
}

func TestCreateSandboxVolumeStoresS3BackendConfigAndRedactsResponse(t *testing.T) {
	repo := newFakeHTTPRepo()
	server := &Server{
		logger:            logrus.New(),
		repo:              repo,
		meteringRepo:      &fakeHTTPMeteringWriter{},
		snapshotMgr:       &fakeHTTPSnapshotManager{},
		s3CredentialCodec: testHTTPS3BackendCredentialCodec(t),
	}

	req := httptest.NewRequest(http.MethodPost, "/sandboxvolumes", bytes.NewReader([]byte(`{
		"access_mode":"RWO",
		"s3":{
			"provider":"cloudflare-r2",
			"bucket":"sandbox-data",
			"prefix":"/team-a/volume-a/",
			"region":"",
			"endpoint_url":"https://account-id.r2.cloudflarestorage.com/",
			"access_key":"access-secret",
			"secret_key":"secret-secret"
		}
	}`)))
	req = req.WithContext(internalauth.WithClaims(req.Context(), &internalauth.Claims{TeamID: "team-1", UserID: "user-1"}))
	recorder := httptest.NewRecorder()

	server.createSandboxVolume(recorder, req)

	if recorder.Code != http.StatusCreated {
		t.Fatalf("status = %d, want %d: %s", recorder.Code, http.StatusCreated, recorder.Body.String())
	}
	if len(repo.createdVolumes) != 1 {
		t.Fatalf("created volumes = %d, want 1", len(repo.createdVolumes))
	}
	created := repo.createdVolumes[0]
	if created.Backend != volume.BackendS3 {
		t.Fatalf("Backend = %q, want %q", created.Backend, volume.BackendS3)
	}
	cfg, err := volume.DecodeS3BackendConfig(created.BackendConfig)
	if err != nil {
		t.Fatalf("DecodeS3BackendConfig() error = %v", err)
	}
	if cfg.Provider != volume.S3ProviderR2 {
		t.Fatalf("provider = %q, want %q", cfg.Provider, volume.S3ProviderR2)
	}
	if cfg.Prefix != "team-a/volume-a" {
		t.Fatalf("prefix = %q, want team-a/volume-a", cfg.Prefix)
	}
	if cfg.EndpointURL != "https://account-id.r2.cloudflarestorage.com" {
		t.Fatalf("endpoint_url = %q", cfg.EndpointURL)
	}
	if cfg.AccessKey != "" || cfg.SecretKey != "" || cfg.SessionToken != "" {
		t.Fatalf("stored config contains plaintext credentials: %+v", cfg)
	}
	if cfg.EncryptedCredentials == nil {
		t.Fatal("stored config missing encrypted credentials")
	}
	if bytes.Contains(created.BackendConfig, []byte("access-secret")) || bytes.Contains(created.BackendConfig, []byte("secret-secret")) {
		t.Fatalf("backend config leaked s3 credentials: %s", created.BackendConfig)
	}
	cfg, err = volume.DecodeS3BackendConfigWithCredentials(req.Context(), created.TeamID, created.ID, created.BackendConfig, server.s3CredentialCodec)
	if err != nil {
		t.Fatalf("DecodeS3BackendConfigWithCredentials() error = %v", err)
	}
	if cfg.AccessKey != "access-secret" || cfg.SecretKey != "secret-secret" {
		t.Fatalf("credentials did not round trip through encrypted backend config")
	}
	if bytes.Contains(recorder.Body.Bytes(), []byte("access-secret")) || bytes.Contains(recorder.Body.Bytes(), []byte("secret-secret")) {
		t.Fatalf("response leaked s3 credentials: %s", recorder.Body.String())
	}
	var envelope struct {
		Data struct {
			Backend string `json:"backend"`
			S3      struct {
				Provider    string `json:"provider"`
				Bucket      string `json:"bucket"`
				Prefix      string `json:"prefix"`
				EndpointURL string `json:"endpoint_url"`
			} `json:"s3"`
		} `json:"data"`
	}
	if err := json.Unmarshal(recorder.Body.Bytes(), &envelope); err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if envelope.Data.Backend != volume.BackendS3 {
		t.Fatalf("response backend = %q, want %q", envelope.Data.Backend, volume.BackendS3)
	}
	if envelope.Data.S3.Provider != volume.S3ProviderR2 || envelope.Data.S3.Bucket != "sandbox-data" || envelope.Data.S3.Prefix != "team-a/volume-a" {
		t.Fatalf("response s3 config = %+v", envelope.Data.S3)
	}
}

func testHTTPS3BackendCredentialCodec(t *testing.T) *volume.S3BackendCredentialCodec {
	t.Helper()
	codec, err := volume.NewS3BackendCredentialCodec("test", map[string][]byte{
		"test": []byte("01234567890123456789012345678901"),
	})
	if err != nil {
		t.Fatalf("NewS3BackendCredentialCodec() error = %v", err)
	}
	return codec
}

func TestCreateSandboxVolumeRejectsUnsupportedS3Combinations(t *testing.T) {
	cases := []struct {
		name string
		body string
	}{
		{
			name: "snapshot",
			body: `{
				"snapshot_id":"snap-1",
				"s3":{"provider":"aws","bucket":"sandbox-data"}
			}`,
		},
		{
			name: "rwx",
			body: `{
				"access_mode":"RWX",
				"s3":{"provider":"aws","bucket":"sandbox-data"}
			}`,
		},
		{
			name: "s0fs with s3 config",
			body: `{
				"backend":"s0fs",
				"s3":{"provider":"aws","bucket":"sandbox-data"}
			}`,
		},
		{
			name: "missing endpoint for r2",
			body: `{
				"s3":{"provider":"r2","bucket":"sandbox-data"}
			}`,
		},
		{
			name: "missing credentials",
			body: `{
				"s3":{"provider":"aws","bucket":"sandbox-data","region":"us-east-1"}
			}`,
		},
	}

	for _, tc := range cases {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			repo := newFakeHTTPRepo()
			server := &Server{
				logger:       logrus.New(),
				repo:         repo,
				meteringRepo: &fakeHTTPMeteringWriter{},
				snapshotMgr:  &fakeHTTPSnapshotManager{},
			}

			req := httptest.NewRequest(http.MethodPost, "/sandboxvolumes", bytes.NewReader([]byte(tc.body)))
			req = req.WithContext(internalauth.WithClaims(req.Context(), &internalauth.Claims{TeamID: "team-1", UserID: "user-1"}))
			recorder := httptest.NewRecorder()

			server.createSandboxVolume(recorder, req)

			if recorder.Code != http.StatusBadRequest {
				t.Fatalf("status = %d, want %d: %s", recorder.Code, http.StatusBadRequest, recorder.Body.String())
			}
			if len(repo.createdVolumes) != 0 {
				t.Fatalf("created volumes = %d, want 0", len(repo.createdVolumes))
			}
		})
	}
}

func TestCreateSandboxVolumeRejectsS3BackendWithoutCredentialCodec(t *testing.T) {
	repo := newFakeHTTPRepo()
	server := &Server{
		logger:       logrus.New(),
		repo:         repo,
		meteringRepo: &fakeHTTPMeteringWriter{},
		snapshotMgr:  &fakeHTTPSnapshotManager{},
	}

	req := httptest.NewRequest(http.MethodPost, "/sandboxvolumes", bytes.NewReader([]byte(`{
		"s3":{
			"provider":"aws",
			"bucket":"sandbox-data",
			"region":"us-east-1",
			"access_key":"access-secret",
			"secret_key":"secret-secret"
		}
	}`)))
	req = req.WithContext(internalauth.WithClaims(req.Context(), &internalauth.Claims{TeamID: "team-1", UserID: "user-1"}))
	recorder := httptest.NewRecorder()

	server.createSandboxVolume(recorder, req)

	if recorder.Code != http.StatusBadRequest {
		t.Fatalf("status = %d, want %d: %s", recorder.Code, http.StatusBadRequest, recorder.Body.String())
	}
	if len(repo.createdVolumes) != 0 {
		t.Fatalf("created volumes = %d, want 0", len(repo.createdVolumes))
	}
}

func TestCreateSandboxVolumeRejectsNullOrBlankSnapshotID(t *testing.T) {
	cases := []struct {
		name string
		body string
	}{
		{name: "null body", body: `null`},
		{name: "null snapshot", body: `{"snapshot_id":null}`},
		{name: "blank snapshot", body: `{"snapshot_id":"   "}`},
	}

	for _, tc := range cases {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			repo := newFakeHTTPRepo()
			snapshotMgr := &fakeHTTPSnapshotManager{}
			server := &Server{
				logger:       logrus.New(),
				repo:         repo,
				meteringRepo: &fakeHTTPMeteringWriter{},
				snapshotMgr:  snapshotMgr,
			}

			req := httptest.NewRequest(http.MethodPost, "/sandboxvolumes", bytes.NewReader([]byte(tc.body)))
			req = req.WithContext(internalauth.WithClaims(req.Context(), &internalauth.Claims{TeamID: "team-1", UserID: "user-1"}))
			recorder := httptest.NewRecorder()

			server.createSandboxVolume(recorder, req)

			if recorder.Code != http.StatusBadRequest {
				t.Fatalf("status = %d, want %d: %s", recorder.Code, http.StatusBadRequest, recorder.Body.String())
			}
			if len(repo.createdVolumes) != 0 {
				t.Fatalf("created volumes = %d, want 0", len(repo.createdVolumes))
			}
			if snapshotMgr.lastCreateVolume != nil {
				t.Fatalf("snapshot create request = %+v, want nil", snapshotMgr.lastCreateVolume)
			}
		})
	}
}

func TestListSandboxVolumesReturnsEmptyArray(t *testing.T) {
	server := &Server{
		logger: logrus.New(),
		repo:   newFakeHTTPRepo(),
	}

	req := httptest.NewRequest(http.MethodGet, "/sandboxvolumes", nil)
	req = req.WithContext(internalauth.WithClaims(req.Context(), &internalauth.Claims{TeamID: "team-1", UserID: "user-1"}))
	recorder := httptest.NewRecorder()

	server.listSandboxVolumes(recorder, req)

	if recorder.Code != http.StatusOK {
		t.Fatalf("status = %d, want %d body=%s", recorder.Code, http.StatusOK, recorder.Body.String())
	}
	resp, apiErr, err := spec.DecodeResponse[[]db.SandboxVolume](recorder.Body)
	if err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if apiErr != nil {
		t.Fatalf("unexpected api error: %+v", apiErr)
	}
	if *resp == nil {
		t.Fatal("volumes slice is nil, want empty array")
	}
	if len(*resp) != 0 {
		t.Fatalf("volumes = %d, want 0", len(*resp))
	}
}

func TestCreateSandboxVolumeFromSnapshot(t *testing.T) {
	repo := newFakeHTTPRepo()
	snapshotMgr := &fakeHTTPSnapshotManager{}
	server := &Server{
		repo:        repo,
		snapshotMgr: snapshotMgr,
		logger:      logrus.New(),
	}
	ctx := internalauth.WithClaims(context.Background(), &internalauth.Claims{
		TeamID: "team-1",
		UserID: "user-1",
	})
	req := httptest.NewRequest(http.MethodPost, "/sandboxvolumes", bytes.NewReader([]byte(`{"snapshot_id":"snap-1","default_posix_uid":1001,"default_posix_gid":1002}`))).WithContext(ctx)
	recorder := httptest.NewRecorder()

	server.createSandboxVolume(recorder, req)

	if recorder.Code != http.StatusCreated {
		t.Fatalf("status = %d, body = %s", recorder.Code, recorder.Body.String())
	}
	if snapshotMgr.lastCreateVolume == nil {
		t.Fatal("expected snapshot-backed volume create request")
	}
	if snapshotMgr.lastCreateVolume.SnapshotID != "snap-1" {
		t.Fatalf("snapshot_id = %q, want snap-1", snapshotMgr.lastCreateVolume.SnapshotID)
	}
	if snapshotMgr.lastCreateVolume.DefaultPosixUID == nil || *snapshotMgr.lastCreateVolume.DefaultPosixUID != 1001 {
		t.Fatalf("default_posix_uid = %v, want 1001", snapshotMgr.lastCreateVolume.DefaultPosixUID)
	}
	if len(repo.volumes) != 0 {
		t.Fatalf("expected handler to delegate snapshot-backed create, repo volumes = %d", len(repo.volumes))
	}
}

func TestListSnapshotsReturnsEmptyArray(t *testing.T) {
	server := &Server{
		logger:      logrus.New(),
		snapshotMgr: &fakeHTTPSnapshotManager{},
	}

	req := httptest.NewRequest(http.MethodGet, "/sandboxvolumes/vol-1/snapshots", nil)
	req.SetPathValue("volume_id", "vol-1")
	req = req.WithContext(internalauth.WithClaims(req.Context(), &internalauth.Claims{TeamID: "team-1", UserID: "user-1"}))
	recorder := httptest.NewRecorder()

	server.listSnapshots(recorder, req)

	if recorder.Code != http.StatusOK {
		t.Fatalf("status = %d, want %d body=%s", recorder.Code, http.StatusOK, recorder.Body.String())
	}
	resp, apiErr, err := spec.DecodeResponse[[]snapshotResponse](recorder.Body)
	if err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if apiErr != nil {
		t.Fatalf("unexpected api error: %+v", apiErr)
	}
	if *resp == nil {
		t.Fatal("snapshots slice is nil, want empty array")
	}
	if len(*resp) != 0 {
		t.Fatalf("snapshots = %d, want 0", len(*resp))
	}
}

func TestCreateSandboxVolumeRejectsPartialDefaultPosixIdentity(t *testing.T) {
	server := &Server{
		logger:       logrus.New(),
		repo:         newFakeHTTPRepo(),
		meteringRepo: &fakeHTTPMeteringWriter{},
		snapshotMgr:  &fakeHTTPSnapshotManager{},
	}

	req := httptest.NewRequest(http.MethodPost, "/sandboxvolumes", bytes.NewReader([]byte(`{"default_posix_uid":1001}`)))
	req = req.WithContext(internalauth.WithClaims(req.Context(), &internalauth.Claims{TeamID: "team-1", UserID: "user-1"}))
	recorder := httptest.NewRecorder()

	server.createSandboxVolume(recorder, req)

	if recorder.Code != http.StatusBadRequest {
		t.Fatalf("status = %d, want %d", recorder.Code, http.StatusBadRequest)
	}
}

func TestCreateOwnedSandboxVolumeStoresOwnerMetadata(t *testing.T) {
	repo := newFakeHTTPRepo()
	server := &Server{
		logger:       logrus.New(),
		repo:         repo,
		meteringRepo: &fakeHTTPMeteringWriter{},
	}

	req := httptest.NewRequest(http.MethodPost, "/internal/v1/sandboxvolumes/owned", bytes.NewReader([]byte(`{"sandbox_id":"sandbox-a","cluster_id":"cluster-a","purpose":"webhook-state"}`)))
	req = req.WithContext(internalauth.WithClaims(req.Context(), &internalauth.Claims{
		Caller:    internalauth.ServiceManager,
		TeamID:    "team-1",
		UserID:    "user-1",
		SandboxID: "sandbox-a",
	}))
	recorder := httptest.NewRecorder()

	server.createOwnedSandboxVolume(recorder, req)

	if recorder.Code != http.StatusCreated {
		t.Fatalf("status = %d, want %d: %s", recorder.Code, http.StatusCreated, recorder.Body.String())
	}
	if len(repo.createdVolumes) != 1 {
		t.Fatalf("created volumes = %d, want 1", len(repo.createdVolumes))
	}
	volumeID := repo.createdVolumes[0].ID
	owner := repo.owners[volumeID]
	if owner == nil {
		t.Fatalf("owner for volume %q was not stored", volumeID)
	}
	if owner.OwnerSandboxID != "sandbox-a" || owner.OwnerClusterID != "cluster-a" || owner.Purpose != "webhook-state" {
		t.Fatalf("unexpected owner: %#v", owner)
	}
}

func TestOwnedSandboxVolumeIsHiddenFromPublicVolumeAPI(t *testing.T) {
	repo := newFakeHTTPRepo()
	server := &Server{logger: logrus.New(), repo: repo}
	repo.volumes["vol-owned"] = &db.SandboxVolume{ID: "vol-owned", TeamID: "team-1", UserID: "user-1"}
	repo.owners["vol-owned"] = &db.SandboxVolumeOwner{
		VolumeID:       "vol-owned",
		OwnerKind:      db.SandboxVolumeOwnerKindSandbox,
		OwnerSandboxID: "sandbox-a",
		OwnerClusterID: "cluster-a",
		Purpose:        "webhook-state",
	}

	req := httptest.NewRequest(http.MethodGet, "/sandboxvolumes/vol-owned", nil)
	req.SetPathValue("id", "vol-owned")
	req = req.WithContext(internalauth.WithClaims(req.Context(), &internalauth.Claims{TeamID: "team-1", UserID: "user-1"}))
	recorder := httptest.NewRecorder()

	server.getSandboxVolume(recorder, req)

	if recorder.Code != http.StatusNotFound {
		t.Fatalf("status = %d, want %d", recorder.Code, http.StatusNotFound)
	}
}

func TestMarkOwnedSandboxVolumesForCleanupIsIdempotent(t *testing.T) {
	repo := newFakeHTTPRepo()
	server := &Server{logger: logrus.New(), repo: repo}
	repo.volumes["vol-owned"] = &db.SandboxVolume{ID: "vol-owned", TeamID: "team-1", UserID: "user-1"}
	repo.owners["vol-owned"] = &db.SandboxVolumeOwner{
		VolumeID:       "vol-owned",
		OwnerKind:      db.SandboxVolumeOwnerKindSandbox,
		OwnerSandboxID: "sandbox-a",
		OwnerClusterID: "cluster-a",
		Purpose:        "webhook-state",
	}

	for i := 0; i < 2; i++ {
		req := httptest.NewRequest(http.MethodPut, "/internal/v1/sandboxvolumes/owned/cleanup", bytes.NewReader([]byte(`{"sandbox_id":"sandbox-a","cluster_id":"cluster-a","reason":"sandbox_deleted"}`)))
		req = req.WithContext(internalauth.WithClaims(req.Context(), &internalauth.Claims{
			Caller:    internalauth.ServiceManager,
			TeamID:    "team-1",
			UserID:    "user-1",
			SandboxID: "sandbox-a",
		}))
		recorder := httptest.NewRecorder()

		server.markOwnedSandboxVolumesForCleanup(recorder, req)

		if recorder.Code != http.StatusOK {
			t.Fatalf("iteration %d status = %d, want %d", i, recorder.Code, http.StatusOK)
		}
	}

	if repo.owners["vol-owned"].CleanupRequestedAt == nil {
		t.Fatal("cleanup_requested_at was not set")
	}
}

func TestPrepareSandboxVolumeForPortalBindCleansIdleDirectMount(t *testing.T) {
	repo := newFakeHTTPRepo()
	repo.volumes["vol-1"] = &db.SandboxVolume{ID: "vol-1", TeamID: "team-1", UserID: "user-1"}
	volMgr := &fakeHTTPVolumeMountManager{
		cleanupDirectFunc: func(context.Context, string) (bool, error) {
			return true, nil
		},
	}
	server := &Server{
		logger: logrus.New(),
		repo:   repo,
		volMgr: volMgr,
	}

	req := httptest.NewRequest(http.MethodPut, "/internal/v1/sandboxvolumes/vol-1/prepare-portal-bind", nil)
	req.SetPathValue("id", "vol-1")
	req = req.WithContext(internalauth.WithClaims(req.Context(), &internalauth.Claims{
		Caller: internalauth.ServiceManager,
		TeamID: "team-1",
		UserID: "user-1",
	}))
	recorder := httptest.NewRecorder()

	server.prepareSandboxVolumeForPortalBind(recorder, req)

	if recorder.Code != http.StatusOK {
		t.Fatalf("status = %d, want %d: %s", recorder.Code, http.StatusOK, recorder.Body.String())
	}
	if volMgr.cleanupCalls != 1 {
		t.Fatalf("cleanup calls = %d, want 1", volMgr.cleanupCalls)
	}
	if volMgr.lastCleanupVolume != "vol-1" {
		t.Fatalf("cleanup volume = %q, want %q", volMgr.lastCleanupVolume, "vol-1")
	}
}

func TestPrepareSandboxVolumeForPortalBindRejectsActiveRWOMounts(t *testing.T) {
	repo := newFakeHTTPRepo()
	repo.volumes["vol-1"] = &db.SandboxVolume{ID: "vol-1", TeamID: "team-1", UserID: "user-1", AccessMode: "RWO"}
	repo.activeMounts["vol-1"] = []*db.VolumeMount{{
		VolumeID:  "vol-1",
		ClusterID: "cluster-a",
		PodID:     "sandbox0-system/ctld-a",
	}}
	server := &Server{
		logger: logrus.New(),
		repo:   repo,
	}

	req := httptest.NewRequest(http.MethodPut, "/internal/v1/sandboxvolumes/vol-1/prepare-portal-bind", nil)
	req.SetPathValue("id", "vol-1")
	req = req.WithContext(internalauth.WithClaims(req.Context(), &internalauth.Claims{
		Caller: internalauth.ServiceManager,
		TeamID: "team-1",
		UserID: "user-1",
	}))
	recorder := httptest.NewRecorder()

	server.prepareSandboxVolumeForPortalBind(recorder, req)

	if recorder.Code != http.StatusConflict {
		t.Fatalf("status = %d, want %d: %s", recorder.Code, http.StatusConflict, recorder.Body.String())
	}
}

func TestPrepareSandboxVolumeForPortalBindReleasesIdleCtldOwner(t *testing.T) {
	repo := newFakeHTTPRepo()
	repo.volumes["vol-1"] = &db.SandboxVolume{ID: "vol-1", TeamID: "team-1", UserID: "user-1", AccessMode: "RWO"}

	var releaseCalls int
	ctld := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/api/v1/volume-portals/owners/release" {
			http.NotFound(w, r)
			return
		}
		releaseCalls++
		repo.activeMounts["vol-1"] = nil
		_ = json.NewEncoder(w).Encode(ctldapi.ReleaseVolumeOwnerResponse{Released: true})
	}))
	defer ctld.Close()
	ctldURL, err := url.Parse(ctld.URL)
	if err != nil {
		t.Fatalf("parse ctld url: %v", err)
	}
	ctldPort, err := strconv.Atoi(ctldURL.Port())
	if err != nil {
		t.Fatalf("parse ctld port: %v", err)
	}

	repo.activeMounts["vol-1"] = []*db.VolumeMount{{
		VolumeID:     "vol-1",
		ClusterID:    "cluster-a",
		PodID:        "sandbox0-system/ctld-a",
		MountOptions: mustMountOptionsRaw(t, volume.MountOptions{OwnerKind: volume.OwnerKindCtld, OwnerPort: ctldPort}),
	}}
	server := &Server{
		logger:      logrus.New(),
		repo:        repo,
		podResolver: &fakeVolumeFilePodResolver{urls: map[string]string{"sandbox0-system/ctld-a": ctld.URL}},
	}

	req := httptest.NewRequest(http.MethodPut, "/internal/v1/sandboxvolumes/vol-1/prepare-portal-bind", nil)
	req.SetPathValue("id", "vol-1")
	req = req.WithContext(internalauth.WithClaims(req.Context(), &internalauth.Claims{
		Caller: internalauth.ServiceManager,
		TeamID: "team-1",
		UserID: "user-1",
	}))
	recorder := httptest.NewRecorder()

	server.prepareSandboxVolumeForPortalBind(recorder, req)

	if recorder.Code != http.StatusOK {
		t.Fatalf("status = %d, want %d: %s", recorder.Code, http.StatusOK, recorder.Body.String())
	}
	if releaseCalls != 1 {
		t.Fatalf("release calls = %d, want 1", releaseCalls)
	}
}

func TestForkVolumePassesDefaultPosixIdentity(t *testing.T) {
	snapshotMgr := &captureForkSnapshotManager{fakeHTTPSnapshotManager: &fakeHTTPSnapshotManager{}}
	server := &Server{
		logger:      logrus.New(),
		repo:        newFakeHTTPRepo(),
		snapshotMgr: snapshotMgr,
	}

	req := httptest.NewRequest(http.MethodPost, "/sandboxvolumes/vol-1/fork", bytes.NewReader([]byte(`{"default_posix_uid":1001,"default_posix_gid":2002}`)))
	req.SetPathValue("id", "vol-1")
	req = req.WithContext(internalauth.WithClaims(req.Context(), &internalauth.Claims{TeamID: "team-1", UserID: "user-1"}))
	recorder := httptest.NewRecorder()

	server.forkVolume(recorder, req)

	if recorder.Code != http.StatusCreated {
		t.Fatalf("status = %d, want %d", recorder.Code, http.StatusCreated)
	}
	if snapshotMgr.lastFork == nil {
		t.Fatalf("ForkVolume request was not captured")
	}
	if snapshotMgr.lastFork.DefaultPosixUID == nil || *snapshotMgr.lastFork.DefaultPosixUID != 1001 {
		t.Fatalf("fork DefaultPosixUID = %v, want 1001", snapshotMgr.lastFork.DefaultPosixUID)
	}
	if snapshotMgr.lastFork.DefaultPosixGID == nil || *snapshotMgr.lastFork.DefaultPosixGID != 2002 {
		t.Fatalf("fork DefaultPosixGID = %v, want 2002", snapshotMgr.lastFork.DefaultPosixGID)
	}
}

func TestForkVolumeReturnsConflictForMountedCtldOwner(t *testing.T) {
	snapshotMgr := &captureForkSnapshotManager{
		fakeHTTPSnapshotManager: &fakeHTTPSnapshotManager{},
		forkErr:                 snapshot.ErrMountedCtldOwner,
	}
	server := &Server{
		logger:      logrus.New(),
		repo:        newFakeHTTPRepo(),
		snapshotMgr: snapshotMgr,
	}

	req := httptest.NewRequest(http.MethodPost, "/sandboxvolumes/vol-1/fork", bytes.NewReader([]byte(`{}`)))
	req.SetPathValue("id", "vol-1")
	req = req.WithContext(internalauth.WithClaims(req.Context(), &internalauth.Claims{TeamID: "team-1", UserID: "user-1"}))
	recorder := httptest.NewRecorder()

	server.forkVolume(recorder, req)

	if recorder.Code != http.StatusConflict {
		t.Fatalf("status = %d, want %d: %s", recorder.Code, http.StatusConflict, recorder.Body.String())
	}
}
