package service_test

import (
	"context"
	"errors"
	"testing"

	"github.com/pingcap/agent-git-service/internal/db"
	"github.com/pingcap/agent-git-service/internal/service"
)

func TestGitHubImportTargetAuthorizationForHumanAndAgent(t *testing.T) {
	svc, ctx, cleanup := setupImportTestService(t)
	defer cleanup()

	human := createImportAuthorizationUser(t, svc, ctx, "human-owner", db.UserKindHuman)
	agent := createImportAuthorizationUser(t, svc, ctx, "migration-agent", db.UserKindAgent)
	organization := createImportAuthorizationOrg(t, svc, ctx, "target-org")
	mustAddOrgMember(t, svc, ctx, organization.ID, agent.ID, db.OrganizationRoleOwner)

	humanCtx := testContextWithUser(ctx, human)
	humanJob, err := svc.CreateGitHubImportJob(humanCtx, service.CreateGitHubImportInput{
		SourceOwner: "source", SourceRepo: "human-repo", TargetOwner: human.Login, TargetRepo: "imported", InitiatorID: human.ID,
		TargetRepository:    service.ImportTargetRepositoryOptions{Description: "preserved source metadata", Visibility: "public", DefaultBranch: "trunk", HasIssues: true, HasWiki: false},
		TargetRepositorySet: true,
	})
	if err != nil {
		t.Fatalf("human creates personal import target: %v", err)
	}
	if humanJob.TargetRepoID == nil || humanJob.TargetMode != service.ImportTargetModeCreate {
		t.Fatalf("human import job target = %#v, want created target", humanJob)
	}
	var humanTarget db.Repository
	if err := svc.DBForCtx(ctx).Take(&humanTarget, "id = ?", *humanJob.TargetRepoID).Error; err != nil {
		t.Fatalf("load created target: %v", err)
	}
	if humanTarget.Description != "preserved source metadata" || humanTarget.Visibility != "public" || humanTarget.DefaultBranch != "trunk" || !humanTarget.HasIssues || humanTarget.HasWiki {
		t.Fatalf("created target settings = %#v", humanTarget)
	}

	agentCtx := testContextWithUser(ctx, agent)
	agentJob, err := svc.CreateGitHubImportJob(agentCtx, service.CreateGitHubImportInput{
		SourceOwner: "source", SourceRepo: "agent-repo", TargetOwner: organization.Login, TargetRepo: "imported", InitiatorID: agent.ID,
	})
	if err != nil {
		t.Fatalf("agent org owner creates import target: %v", err)
	}
	if _, err := svc.AuthorizeImportJob(agentCtx, agentJob.ID, agent.ID, service.ImportJobPermissionManage); err != nil {
		t.Fatalf("agent administers its import job: %v", err)
	}
}

func TestGitHubImportTargetAuthorizationRejectsUnauthorizedAndNonEmptyTargets(t *testing.T) {
	svc, ctx, cleanup := setupImportTestService(t)
	defer cleanup()

	owner := createImportAuthorizationUser(t, svc, ctx, "org-owner", db.UserKindHuman)
	member := createImportAuthorizationUser(t, svc, ctx, "org-member", db.UserKindHuman)
	outsider := createImportAuthorizationUser(t, svc, ctx, "outsider", db.UserKindHuman)
	organization := createImportAuthorizationOrg(t, svc, ctx, "target-org")
	mustAddOrgMember(t, svc, ctx, organization.ID, owner.ID, db.OrganizationRoleOwner)
	mustAddOrgMember(t, svc, ctx, organization.ID, member.ID, db.OrganizationRoleMember)

	memberCtx := testContextWithUser(ctx, member)
	if _, err := svc.CreateGitHubImportJob(memberCtx, service.CreateGitHubImportInput{
		SourceOwner: "source", SourceRepo: "repo", TargetOwner: organization.Login, TargetRepo: "denied", InitiatorID: member.ID,
	}); !errors.Is(err, service.ErrForbidden) {
		t.Fatalf("member create org target error = %v, want ErrForbidden", err)
	}

	ownerCtx := testContextWithUser(ctx, owner)
	repo, err := svc.CreateRepo(ownerCtx, service.CreateRepoInput{
		OwnerLogin: organization.Login, Name: "existing", Visibility: "private", RequireOrgAdmin: true,
	})
	if err != nil {
		t.Fatalf("create existing repo: %v", err)
	}
	if err := svc.DBForCtx(ctx).Create(&db.Collaborator{RepositoryID: repo.ID, UserID: outsider.ID, Permission: "admin"}).Error; err != nil {
		t.Fatalf("grant repository admin: %v", err)
	}

	outsiderCtx := testContextWithUser(ctx, outsider)
	job, err := svc.CreateGitHubImportJob(outsiderCtx, service.CreateGitHubImportInput{
		SourceOwner: "source", SourceRepo: "repo", TargetOwner: organization.Login, TargetRepo: repo.Name,
		TargetMode: service.ImportTargetModeExistingEmpty, InitiatorID: outsider.ID,
	})
	if err != nil {
		t.Fatalf("repository admin imports into empty existing repo: %v", err)
	}
	if job.TargetRepoID == nil || *job.TargetRepoID != repo.ID {
		t.Fatalf("existing target id = %#v, want %d", job.TargetRepoID, repo.ID)
	}
	if _, err := svc.AuthorizeImportJob(testContextWithUser(ctx, member), job.ID, member.ID, service.ImportJobPermissionManage); !errors.Is(err, service.ErrForbidden) {
		t.Fatalf("ordinary member job authorization error = %v, want ErrForbidden", err)
	}

	if _, err := svc.CancelImportJob(outsiderCtx, job.ID); err != nil {
		t.Fatalf("cancel empty-target job: %v", err)
	}
	if err := svc.DBForCtx(ctx).Create(&db.Label{RepositoryID: repo.ID, Name: "already-used", Color: "ffffff"}).Error; err != nil {
		t.Fatalf("seed nonempty existing repository: %v", err)
	}
	if _, err := svc.CreateGitHubImportJob(outsiderCtx, service.CreateGitHubImportInput{
		SourceOwner: "source", SourceRepo: "repo-two", TargetOwner: organization.Login, TargetRepo: repo.Name,
		TargetMode: service.ImportTargetModeExistingEmpty, InitiatorID: outsider.ID,
	}); !errors.Is(err, service.ErrConflict) {
		t.Fatalf("nonempty target error = %v, want ErrConflict", err)
	}
}

func TestGitHubImportTargetReservationPreventsConcurrentTargetUse(t *testing.T) {
	svc, ctx, cleanup := setupImportTestService(t)
	defer cleanup()

	owner := createImportAuthorizationUser(t, svc, ctx, "owner", db.UserKindHuman)
	ownerCtx := testContextWithUser(ctx, owner)
	first, err := svc.CreateGitHubImportJob(ownerCtx, service.CreateGitHubImportInput{
		SourceOwner: "source", SourceRepo: "first", TargetOwner: owner.Login, TargetRepo: "destination", InitiatorID: owner.ID,
	})
	if err != nil {
		t.Fatalf("create first import: %v", err)
	}
	if _, err := svc.CreateGitHubImportJob(ownerCtx, service.CreateGitHubImportInput{
		SourceOwner: "source", SourceRepo: "second", TargetOwner: owner.Login, TargetRepo: "destination",
		TargetMode: service.ImportTargetModeExistingEmpty, InitiatorID: owner.ID,
	}); !errors.Is(err, service.ErrConflict) {
		t.Fatalf("second import into reserved target error = %v, want ErrConflict", err)
	}
	if _, err := svc.CancelImportJob(ownerCtx, first.ID); err != nil {
		t.Fatalf("cancel first import: %v", err)
	}
	if _, err := svc.CreateGitHubImportJob(ownerCtx, service.CreateGitHubImportInput{
		SourceOwner: "source", SourceRepo: "second", TargetOwner: owner.Login, TargetRepo: "destination",
		TargetMode: service.ImportTargetModeExistingEmpty, InitiatorID: owner.ID,
	}); err != nil {
		t.Fatalf("target is reusable after cancellation: %v", err)
	}
}

func createImportAuthorizationUser(t *testing.T, svc *service.Service, ctx context.Context, login, kind string) db.User {
	t.Helper()
	user := db.User{Login: login, Name: login, Type: db.TypeUser, UserKind: kind, Status: db.UserStatusActive}
	if err := svc.DBForCtx(ctx).Create(&user).Error; err != nil {
		t.Fatalf("create user %s: %v", login, err)
	}
	return user
}

func createImportAuthorizationOrg(t *testing.T, svc *service.Service, ctx context.Context, login string) db.User {
	t.Helper()
	organization := db.User{Login: login, Name: login, Type: db.TypeOrganization, UserKind: db.UserKindHuman, Status: db.UserStatusActive}
	if err := svc.DBForCtx(ctx).Create(&organization).Error; err != nil {
		t.Fatalf("create organization %s: %v", login, err)
	}
	return organization
}
