package service

import (
	"context"
	"errors"
	"testing"
	"time"

	"github.com/google/uuid"
	"github.com/ocdp/cluster-service/internal/adapter/output/persistence/mock"
	"github.com/ocdp/cluster-service/internal/domain/entity"
	"github.com/ocdp/cluster-service/internal/domain/repository"
	"github.com/ocdp/cluster-service/internal/pkg/authz"
	jwtpkg "github.com/ocdp/cluster-service/internal/pkg/jwt"
)

func TestAuthServiceUpdateUserDowngradeReusesUsernameWorkspace(t *testing.T) {
	ctx := adminContext()
	userRepo := mock.NewUserRepositoryMock()
	workspaceRepo := mock.NewWorkspaceRepositoryMock()
	svc := NewAuthService(userRepo, workspaceRepo, testPasswordHasher{}, testTokenGenerator{})

	target := testUser("user-1", "alice", authz.RoleAdmin, entity.DefaultWorkspaceID)
	if err := userRepo.Create(ctx, target); err != nil {
		t.Fatalf("seed user: %v", err)
	}
	workspace := entity.NewWorkspace(userWorkspaceName("alice"), "admin")
	workspace.ID = "workspace-alice"
	workspace.K8sNamespace = entity.NamespaceForUser("alice")
	workspace.K8sSAName = entity.ServiceAccountForNamespace(workspace.K8sNamespace)
	if err := workspaceRepo.Create(ctx, workspace); err != nil {
		t.Fatalf("seed workspace: %v", err)
	}

	updated, err := svc.UpdateUser(ctx, target.ID, authz.RoleUser, "", UserWorkspaceOptions{DefaultClusterID: "cluster-1"}, nil, nil)
	if err != nil {
		t.Fatalf("UpdateUser returned error: %v", err)
	}

	if updated.Role != authz.RoleUser {
		t.Fatalf("expected user role, got %q", updated.Role)
	}
	if updated.WorkspaceID != workspace.ID {
		t.Fatalf("expected reused workspace %q, got %q", workspace.ID, updated.WorkspaceID)
	}
	reused, err := workspaceRepo.GetByID(ctx, workspace.ID)
	if err != nil {
		t.Fatalf("get reused workspace: %v", err)
	}
	if reused.DefaultClusterID != "cluster-1" {
		t.Fatalf("expected updated default cluster, got %q", reused.DefaultClusterID)
	}
}

func TestAuthServiceRegisterUserAlwaysCreatesPrivateWorkspaceWithZeroDefaultQuotas(t *testing.T) {
	ctx := adminContext()
	userRepo := mock.NewUserRepositoryMock()
	workspaceRepo := mock.NewWorkspaceRepositoryMock()
	svc := NewAuthService(userRepo, workspaceRepo, testPasswordHasher{}, testTokenGenerator{})

	user, err := svc.Register(ctx, "alice", "password", authz.RoleUser, "shared-workspace", UserWorkspaceOptions{}, nil, nil)
	if err != nil {
		t.Fatalf("Register returned error: %v", err)
	}
	if user.WorkspaceID == "shared-workspace" || user.WorkspaceID == entity.DefaultWorkspaceID {
		t.Fatalf("expected private user workspace, got %q", user.WorkspaceID)
	}
	workspace, err := workspaceRepo.GetByID(ctx, user.WorkspaceID)
	if err != nil {
		t.Fatalf("get user workspace: %v", err)
	}
	if workspace.K8sNamespace != entity.NamespaceForUser("alice") {
		t.Fatalf("expected user namespace %q, got %q", entity.NamespaceForUser("alice"), workspace.K8sNamespace)
	}
	if workspace.QuotaCPU != "" || workspace.QuotaMemory != "" || workspace.QuotaGPU != "0" || workspace.QuotaGPUMem != "0" {
		t.Fatalf("expected omitted CPU/memory to stay unlimited and GPU/gpumem to default zero, got cpu=%q memory=%q gpu=%q gpumem=%q", workspace.QuotaCPU, workspace.QuotaMemory, workspace.QuotaGPU, workspace.QuotaGPUMem)
	}
}

func TestAuthServiceUpdateUserDowngradeRejectsNamespaceConflict(t *testing.T) {
	ctx := adminContext()
	userRepo := mock.NewUserRepositoryMock()
	workspaceRepo := mock.NewWorkspaceRepositoryMock()
	svc := NewAuthService(userRepo, workspaceRepo, testPasswordHasher{}, testTokenGenerator{})

	target := testUser("user-1", "alice", authz.RoleAdmin, entity.DefaultWorkspaceID)
	if err := userRepo.Create(ctx, target); err != nil {
		t.Fatalf("seed user: %v", err)
	}
	conflicting := entity.NewWorkspace("someone-else", "admin")
	conflicting.ID = "workspace-other"
	conflicting.K8sNamespace = entity.NamespaceForUser("alice")
	conflicting.K8sSAName = entity.ServiceAccountForNamespace(conflicting.K8sNamespace)
	if err := workspaceRepo.Create(ctx, conflicting); err != nil {
		t.Fatalf("seed conflicting workspace: %v", err)
	}

	_, err := svc.UpdateUser(ctx, target.ID, authz.RoleUser, "", UserWorkspaceOptions{}, nil, nil)
	if !errors.Is(err, entity.ErrWorkspaceNamespaceConflict) {
		t.Fatalf("expected namespace conflict, got %v", err)
	}
}

func TestAuthServiceDeleteUserRejectsUserWithInstances(t *testing.T) {
	ctx := adminContext()
	userRepo := mock.NewUserRepositoryMock()
	workspaceRepo := mock.NewWorkspaceRepositoryMock()
	instanceRepo := mock.NewInstanceRepositoryMock()
	svc := NewAuthService(userRepo, workspaceRepo, testPasswordHasher{}, testTokenGenerator{})
	svc.SetUserLifecycleCleanup(instanceRepo, nil, nil, nil)

	user := testUser("user-1", "alice", authz.RoleUser, "workspace-alice")
	if err := userRepo.Create(ctx, user); err != nil {
		t.Fatalf("seed user: %v", err)
	}
	instance := entity.NewInstance("cluster-1", "app", "ocdp-u-alice", "registry-1", "repo", "chart", "1.0.0")
	instance.ID = "instance-1"
	instance.OwnerID = user.ID
	instance.WorkspaceID = user.WorkspaceID
	if err := instanceRepo.Create(ctx, instance); err != nil {
		t.Fatalf("seed instance: %v", err)
	}

	err := svc.DeleteUser(ctx, user.ID)
	if !errors.Is(err, entity.ErrUserHasInstances) {
		t.Fatalf("expected user instance conflict, got %v", err)
	}
	if _, err := userRepo.GetByID(ctx, user.ID); err != nil {
		t.Fatalf("user should not be deleted: %v", err)
	}
}

func TestAuthServiceDeleteUserRejectsWorkspaceInstanceEvenWithDifferentOwner(t *testing.T) {
	ctx := adminContext()
	userRepo := mock.NewUserRepositoryMock()
	workspaceRepo := mock.NewWorkspaceRepositoryMock()
	instanceRepo := mock.NewInstanceRepositoryMock()
	svc := NewAuthService(userRepo, workspaceRepo, testPasswordHasher{}, testTokenGenerator{})
	svc.SetUserLifecycleCleanup(instanceRepo, nil, nil, nil)

	user := testUser("user-1", "alice", authz.RoleUser, "workspace-alice")
	if err := userRepo.Create(ctx, user); err != nil {
		t.Fatalf("seed user: %v", err)
	}
	instance := entity.NewInstance("cluster-1", "shared-workspace-app", "ocdp-u-alice", "registry-1", "repo", "chart", "1.0.0")
	instance.ID = "instance-1"
	instance.OwnerID = "other-user"
	instance.WorkspaceID = user.WorkspaceID
	if err := instanceRepo.Create(ctx, instance); err != nil {
		t.Fatalf("seed workspace instance: %v", err)
	}

	err := svc.DeleteUser(ctx, user.ID)
	if !errors.Is(err, entity.ErrUserHasInstances) {
		t.Fatalf("expected workspace instance conflict, got %v", err)
	}
	if _, err := userRepo.GetByID(ctx, user.ID); err != nil {
		t.Fatalf("user should not be deleted: %v", err)
	}
}

func TestAuthServiceDeleteUserCleansExclusiveWorkspaceBindings(t *testing.T) {
	ctx := adminContext()
	userRepo := mock.NewUserRepositoryMock()
	workspaceRepo := mock.NewWorkspaceRepositoryMock()
	instanceRepo := mock.NewInstanceRepositoryMock()
	bindingRepo := mock.NewWorkspaceClusterBindingRepositoryMock()
	clusterRepo := &testClusterRepo{clusters: map[string]*entity.Cluster{
		"cluster-1": {ID: "cluster-1", Name: "cluster-1", Host: "https://cluster.invalid", Token: "token"},
	}}
	tenantClient := &recordingTenantClient{}
	svc := NewAuthService(userRepo, workspaceRepo, testPasswordHasher{}, testTokenGenerator{})
	svc.SetUserLifecycleCleanup(instanceRepo, clusterRepo, bindingRepo, tenantClient)

	workspace := entity.NewWorkspace(userWorkspaceName("alice"), "admin")
	workspace.ID = "workspace-alice"
	workspace.K8sNamespace = entity.NamespaceForUser("alice")
	workspace.K8sSAName = entity.ServiceAccountForNamespace(workspace.K8sNamespace)
	if err := workspaceRepo.Create(ctx, workspace); err != nil {
		t.Fatalf("seed workspace: %v", err)
	}
	user := testUser("user-1", "alice", authz.RoleUser, workspace.ID)
	if err := userRepo.Create(ctx, user); err != nil {
		t.Fatalf("seed user: %v", err)
	}
	if err := bindingRepo.Upsert(ctx, &entity.WorkspaceClusterBinding{
		ID:             "binding-1",
		WorkspaceID:    workspace.ID,
		ClusterID:      "cluster-1",
		Namespace:      workspace.K8sNamespace,
		ServiceAccount: workspace.K8sSAName,
		Status:         "active",
	}); err != nil {
		t.Fatalf("seed binding: %v", err)
	}

	if err := svc.DeleteUser(ctx, user.ID); err != nil {
		t.Fatalf("DeleteUser returned error: %v", err)
	}
	if _, err := userRepo.GetByID(ctx, user.ID); !errors.Is(err, entity.ErrUserNotFound) {
		t.Fatalf("expected user deleted, got %v", err)
	}
	if bindings, err := bindingRepo.ListByWorkspace(ctx, workspace.ID); err != nil || len(bindings) != 0 {
		t.Fatalf("expected bindings cleaned, got len=%d err=%v", len(bindings), err)
	}
	if len(tenantClient.deleted) != 1 || tenantClient.deleted[0] != workspace.K8sNamespace {
		t.Fatalf("expected tenant namespace cleanup, got %#v", tenantClient.deleted)
	}
	if _, err := workspaceRepo.GetByID(ctx, workspace.ID); !errors.Is(err, entity.ErrWorkspaceNotFound) {
		t.Fatalf("expected exclusive workspace deleted, got %v", err)
	}
}

func adminContext() context.Context {
	return authz.WithPrincipal(context.Background(), &authz.Principal{
		UserID:      "admin-1",
		Username:    "admin",
		Role:        authz.RoleAdmin,
		WorkspaceID: entity.DefaultWorkspaceID,
	})
}

func testUser(id, username, role, workspaceID string) *entity.User {
	user := entity.NewUser(username, "hash", username+"@local.ocdp")
	user.ID = id
	user.Role = role
	user.WorkspaceID = workspaceID
	return user
}

type testPasswordHasher struct{}

func (testPasswordHasher) Hash(password string) (string, error) { return "hash:" + password, nil }
func (testPasswordHasher) Verify(password, hash string) error   { return nil }

type testTokenGenerator struct{}

func (testTokenGenerator) Generate(userID, username, role, workspaceID string) (string, string, error) {
	return "access", "refresh", nil
}
func (testTokenGenerator) Verify(token string) (string, string, error) { return "", "", nil }
func (testTokenGenerator) VerifyWithIssuedAt(token string) (string, string, int64, error) {
	return "", "", 0, nil
}
func (testTokenGenerator) VerifyAccess(token string) (*jwtpkg.Claims, error)  { return nil, nil }
func (testTokenGenerator) VerifyRefresh(token string) (*jwtpkg.Claims, error) { return nil, nil }
func (testTokenGenerator) Refresh(refreshToken string) (string, error)        { return "access", nil }

type testClusterRepo struct {
	clusters map[string]*entity.Cluster
}

func (r *testClusterRepo) Create(ctx context.Context, cluster *entity.Cluster) error {
	if cluster.ID == "" {
		cluster.ID = uuid.New().String()
	}
	copy := *cluster
	r.clusters[cluster.ID] = &copy
	return nil
}
func (r *testClusterRepo) GetByID(ctx context.Context, id string) (*entity.Cluster, error) {
	cluster, ok := r.clusters[id]
	if !ok {
		return nil, entity.ErrClusterNotFound
	}
	copy := *cluster
	return &copy, nil
}
func (r *testClusterRepo) GetByName(ctx context.Context, name string) (*entity.Cluster, error) {
	for _, cluster := range r.clusters {
		if cluster.Name == name {
			copy := *cluster
			return &copy, nil
		}
	}
	return nil, entity.ErrClusterNotFound
}
func (r *testClusterRepo) Update(ctx context.Context, cluster *entity.Cluster) error {
	copy := *cluster
	r.clusters[cluster.ID] = &copy
	return nil
}
func (r *testClusterRepo) Delete(ctx context.Context, id string) error {
	delete(r.clusters, id)
	return nil
}
func (r *testClusterRepo) List(ctx context.Context) ([]*entity.Cluster, error) {
	result := make([]*entity.Cluster, 0, len(r.clusters))
	for _, cluster := range r.clusters {
		copy := *cluster
		result = append(result, &copy)
	}
	return result, nil
}

type recordingTenantClient struct {
	deleted []string
	usage   *repository.ResourceQuotaUsage
}

func (c *recordingTenantClient) EnsureTenant(ctx context.Context, cluster *entity.Cluster, binding entity.TenantBinding) error {
	return nil
}
func (c *recordingTenantClient) IssueKubeconfig(ctx context.Context, cluster *entity.Cluster, binding entity.TenantBinding, ttl time.Duration) (*entity.TenantKubeconfig, error) {
	return nil, nil
}
func (c *recordingTenantClient) GetResourceQuotaUsage(ctx context.Context, cluster *entity.Cluster, binding entity.TenantBinding) (*repository.ResourceQuotaUsage, error) {
	if c.usage != nil {
		return c.usage, nil
	}
	return &repository.ResourceQuotaUsage{}, nil
}
func (c *recordingTenantClient) SuspendTenant(ctx context.Context, cluster *entity.Cluster, binding entity.TenantBinding) error {
	return nil
}
func (c *recordingTenantClient) DeleteTenant(ctx context.Context, cluster *entity.Cluster, binding entity.TenantBinding) error {
	if err := binding.Validate(); err != nil {
		return err
	}
	c.deleted = append(c.deleted, binding.Namespace)
	return nil
}
