package services

import (
	"context"
	"testing"

	"clawreef/internal/models"
)

// stubInstanceVisibilityRepo is a minimal InstanceRepository used by the
// visibility tests below. It only implements the read paths exercised by
// GetByUserID / GetAllInstances; every other method panics so that any
// accidental call surfaces as a test failure instead of a silent stub.
type stubInstanceVisibilityRepo struct {
	all []models.Instance
}

func (r *stubInstanceVisibilityRepo) byUser(userID int) []models.Instance {
	out := make([]models.Instance, 0, len(r.all))
	for _, inst := range r.all {
		if inst.UserID == userID {
			out = append(out, inst)
		}
	}
	return out
}

func (r *stubInstanceVisibilityRepo) Create(*models.Instance) error { panic("not used") }
func (r *stubInstanceVisibilityRepo) GetByID(int) (*models.Instance, error) {
	panic("not used")
}
func (r *stubInstanceVisibilityRepo) GetByAccessToken(string) (*models.Instance, error) {
	panic("not used")
}
func (r *stubInstanceVisibilityRepo) GetByAgentBootstrapToken(string) (*models.Instance, error) {
	panic("not used")
}
func (r *stubInstanceVisibilityRepo) GetAll(offset, limit int) ([]models.Instance, error) {
	return paginate(r.all, offset, limit), nil
}
func (r *stubInstanceVisibilityRepo) CountAll() (int, error) { return len(r.all), nil }
func (r *stubInstanceVisibilityRepo) GetByUserID(userID, offset, limit int) ([]models.Instance, error) {
	return paginate(r.byUser(userID), offset, limit), nil
}
func (r *stubInstanceVisibilityRepo) CountByUserID(userID int) (int, error) {
	return len(r.byUser(userID)), nil
}
func (r *stubInstanceVisibilityRepo) CountActiveByMode(context.Context, string) (int, error) {
	return 0, nil
}
func (r *stubInstanceVisibilityRepo) ExistsByUserIDAndName(int, string) (bool, error) {
	return false, nil
}
func (r *stubInstanceVisibilityRepo) GetAllRunning() ([]models.Instance, error) {
	return nil, nil
}
func (r *stubInstanceVisibilityRepo) GetV2DesiredRunning(context.Context, int) ([]models.Instance, error) {
	panic("not used")
}
func (r *stubInstanceVisibilityRepo) GetV2Creating(context.Context, int) ([]models.Instance, error) {
	panic("not used")
}
func (r *stubInstanceVisibilityRepo) UpdateRuntimeState(context.Context, int, string, int, *string) error {
	panic("not used")
}
func (r *stubInstanceVisibilityRepo) SetWorkspacePath(context.Context, int, string) error {
	panic("not used")
}
func (r *stubInstanceVisibilityRepo) UpdateWorkspaceUsage(context.Context, int, int64) error {
	panic("not used")
}
func (r *stubInstanceVisibilityRepo) Update(*models.Instance) error { panic("not used") }
func (r *stubInstanceVisibilityRepo) Delete(int) error              { panic("not used") }

func paginate(items []models.Instance, offset, limit int) []models.Instance {
	if offset >= len(items) {
		return []models.Instance{}
	}
	end := offset + limit
	if end > len(items) {
		end = len(items)
	}
	out := make([]models.Instance, end-offset)
	copy(out, items[offset:end])
	return out
}

// TestGetByUserIDFiltersByCaller locks in the workspace-view contract: the
// caller-scoped listing must return only the caller's own instances,
// regardless of any role the caller may hold elsewhere. Regression guard
// for the admin-cross-user-leakage bug in which role=admin widened this
// endpoint to every user's instances.
func TestGetByUserIDFiltersByCaller(t *testing.T) {
	t.Parallel()

	repo := &stubInstanceVisibilityRepo{
		all: []models.Instance{
			{ID: 1, UserID: 10, Name: "alice-1"},
			{ID: 2, UserID: 10, Name: "alice-2"},
			{ID: 3, UserID: 20, Name: "bob-1"},
		},
	}
	svc := &instanceService{instanceRepo: repo}

	instances, total, err := svc.GetByUserID(10, 0, 50)
	if err != nil {
		t.Fatalf("GetByUserID returned error: %v", err)
	}
	if total != 2 {
		t.Fatalf("expected total=2 for alice, got %d", total)
	}
	if len(instances) != 2 || instances[0].Name != "alice-1" || instances[1].Name != "alice-2" {
		t.Fatalf("expected alice's instances only, got %+v", instances)
	}

	instances, total, err = svc.GetByUserID(20, 0, 50)
	if err != nil {
		t.Fatalf("GetByUserID returned error: %v", err)
	}
	if total != 1 {
		t.Fatalf("expected total=1 for bob, got %d", total)
	}
	if len(instances) != 1 || instances[0].Name != "bob-1" {
		t.Fatalf("expected bob's instance only, got %+v", instances)
	}

	// A user with no instances sees an empty list, never someone else's.
	instances, total, err = svc.GetByUserID(99, 0, 50)
	if err != nil {
		t.Fatalf("GetByUserID returned error: %v", err)
	}
	if total != 0 || len(instances) != 0 {
		t.Fatalf("expected empty listing for user 99, got total=%d instances=%+v", total, instances)
	}
}

// TestGetAllInstancesReturnsEveryUser verifies the admin-console surface:
// cross-user listing, with pagination, independent of caller identity (the
// admin middleware gates reachability at the route layer, not here).
func TestGetAllInstancesReturnsEveryUser(t *testing.T) {
	t.Parallel()

	repo := &stubInstanceVisibilityRepo{
		all: []models.Instance{
			{ID: 1, UserID: 10, Name: "alice-1"},
			{ID: 2, UserID: 10, Name: "alice-2"},
			{ID: 3, UserID: 20, Name: "bob-1"},
		},
	}
	svc := &instanceService{instanceRepo: repo}

	instances, total, err := svc.GetAllInstances(0, 50)
	if err != nil {
		t.Fatalf("GetAllInstances returned error: %v", err)
	}
	if total != 3 {
		t.Fatalf("expected total=3 across users, got %d", total)
	}
	if len(instances) != 3 {
		t.Fatalf("expected 3 instances, got %d", len(instances))
	}

	// Pagination still respected.
	page1, _, err := svc.GetAllInstances(0, 2)
	if err != nil {
		t.Fatalf("GetAllInstances page1 error: %v", err)
	}
	if len(page1) != 2 {
		t.Fatalf("expected page1 size=2, got %d", len(page1))
	}
	page2, _, err := svc.GetAllInstances(2, 2)
	if err != nil {
		t.Fatalf("GetAllInstances page2 error: %v", err)
	}
	if len(page2) != 1 || page2[0].Name != "bob-1" {
		t.Fatalf("expected page2=[bob-1], got %+v", page2)
	}
}
