package service

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

	"github.com/stretchr/testify/require"

	coremodel "security-center-backend/internal/model"
	vulnmodel "security-center-backend/internal/modules/vuln/model"
	"security-center-backend/internal/modules/vuln/repository"
	corerepo "security-center-backend/internal/repository"
	"security-center-backend/internal/service/directory"
)

type accessLifecycleVulnRepo struct {
	vuln             *vulnmodel.Vulnerability
	created          *vulnmodel.Vulnerability
	lastUpdateFields map[string]interface{}
	findByIDCalls    int
}

func (r *accessLifecycleVulnRepo) List(ctx context.Context, params repository.ListParams) ([]vulnmodel.Vulnerability, int64, error) {
	return nil, 0, nil
}

func (r *accessLifecycleVulnRepo) Create(ctx context.Context, vuln *vulnmodel.Vulnerability) error {
	r.created = vuln
	return nil
}

func (r *accessLifecycleVulnRepo) FindByID(ctx context.Context, id uint64) (*vulnmodel.Vulnerability, error) {
	r.findByIDCalls++
	if r.vuln == nil || r.vuln.ID != id {
		return nil, errors.New("not found")
	}
	copy := *r.vuln
	return &copy, nil
}

func (r *accessLifecycleVulnRepo) FindByPublicID(ctx context.Context, publicID string) (*vulnmodel.Vulnerability, error) {
	if r.vuln == nil || strings.TrimSpace(r.vuln.PublicID) != strings.TrimSpace(publicID) {
		return nil, errors.New("not found")
	}
	copy := *r.vuln
	return &copy, nil
}

func (r *accessLifecycleVulnRepo) Update(ctx context.Context, vuln *vulnmodel.Vulnerability) error {
	return nil
}

func (r *accessLifecycleVulnRepo) UpdateFields(ctx context.Context, id uint64, fields map[string]interface{}) error {
	if r.vuln == nil || r.vuln.ID != id {
		return errors.New("not found")
	}
	r.lastUpdateFields = map[string]interface{}{}
	for k, v := range fields {
		r.lastUpdateFields[k] = v
	}
	if status, ok := fields["status"].(vulnmodel.VulnerabilityStatus); ok {
		r.vuln.Status = status
	}
	if status, ok := fields["status"].(string); ok {
		r.vuln.Status = vulnmodel.VulnerabilityStatus(status)
	}
	if ts, ok := fields["claimed_at"].(time.Time); ok {
		r.vuln.ClaimedAt = &ts
	}
	if ts, ok := fields["fix_requested_at"].(time.Time); ok {
		r.vuln.FixRequestedAt = &ts
	}
	if ts, ok := fields["fixed_at"].(time.Time); ok {
		r.vuln.FixedAt = &ts
	}
	if _, ok := fields["fixed_at"]; ok && fields["fixed_at"] == nil {
		r.vuln.FixedAt = nil
	}
	if ts, ok := fields["closed_at"].(time.Time); ok {
		r.vuln.ClosedAt = &ts
	}
	if _, ok := fields["closed_at"]; ok && fields["closed_at"] == nil {
		r.vuln.ClosedAt = nil
	}
	if _, ok := fields["fix_requested_at"]; ok && fields["fix_requested_at"] == nil {
		r.vuln.FixRequestedAt = nil
	}
	if ids, ok := fields["authorized_viewer_employee_ids"].(vulnmodel.UInt64Array); ok {
		r.vuln.AuthorizedViewerEmployeeIDs = ids
	}
	if isPublicViewable, ok := fields["is_public_viewable"].(bool); ok {
		r.vuln.IsPublicViewable = isPublicViewable
	}
	return nil
}

type accessLifecycleEventRepo struct {
	events []vulnmodel.VulnerabilityEvent
}

func (r *accessLifecycleEventRepo) Create(ctx context.Context, event *vulnmodel.VulnerabilityEvent) error {
	if event != nil {
		r.events = append(r.events, *event)
	}
	return nil
}

func (r *accessLifecycleEventRepo) List(ctx context.Context, vulnID uint64) ([]vulnmodel.VulnerabilityEvent, error) {
	return append([]vulnmodel.VulnerabilityEvent(nil), r.events...), nil
}

type accessLifecycleUserRepo struct {
	users map[uint64]*coremodel.User
}

func (r *accessLifecycleUserRepo) FindByID(ctx context.Context, id uint64) (*coremodel.User, error) {
	if user, ok := r.users[id]; ok && user != nil {
		copy := *user
		return &copy, nil
	}
	return nil, errors.New("not found")
}

func (r *accessLifecycleUserRepo) FindByAuth0ID(ctx context.Context, auth0UserID string) (*coremodel.User, error) {
	return nil, errors.New("not implemented")
}

func (r *accessLifecycleUserRepo) FindByExternalID(ctx context.Context, externalID string) (*coremodel.User, error) {
	return nil, errors.New("not implemented")
}

func (r *accessLifecycleUserRepo) FindByEmail(ctx context.Context, email string) (*coremodel.User, error) {
	return nil, errors.New("not implemented")
}

func (r *accessLifecycleUserRepo) FindByEmails(ctx context.Context, emails []string) ([]coremodel.User, error) {
	return nil, errors.New("not implemented")
}

func (r *accessLifecycleUserRepo) CreateOrUpdate(ctx context.Context, user *coremodel.User) error {
	return nil
}

func (r *accessLifecycleUserRepo) List(ctx context.Context, filter corerepo.UserFilter, page, pageSize int) ([]coremodel.User, int64, error) {
	return nil, 0, errors.New("not implemented")
}

func (r *accessLifecycleUserRepo) AssignRoles(ctx context.Context, userID uint64, roleIDs []uint64) error {
	return nil
}

func (r *accessLifecycleUserRepo) RemoveRole(ctx context.Context, userID uint64, roleID uint64) error {
	return nil
}

func (r *accessLifecycleUserRepo) GetUserRoles(ctx context.Context, userID uint64) ([]coremodel.Role, error) {
	return nil, errors.New("not implemented")
}

func (r *accessLifecycleUserRepo) GetUserPermissions(ctx context.Context, userID uint64) ([]coremodel.Permission, error) {
	return nil, errors.New("not implemented")
}

func (r *accessLifecycleUserRepo) UpdateDefaultRole(ctx context.Context, userID uint64, roleID *uint64) error {
	return nil
}

type noopEmployeeDirectory struct{}

func (noopEmployeeDirectory) FindByID(ctx context.Context, id uint64) (*directory.Employee, error) {
	return nil, nil
}

func (noopEmployeeDirectory) FindByEmail(ctx context.Context, email string) (*directory.Employee, error) {
	return nil, nil
}

func (noopEmployeeDirectory) FindByEmails(ctx context.Context, emails []string) ([]directory.Employee, error) {
	return []directory.Employee{}, nil
}

func (noopEmployeeDirectory) SearchEmployees(ctx context.Context, query string) ([]directory.Employee, error) {
	return []directory.Employee{}, nil
}

func (noopEmployeeDirectory) ListEmployees(ctx context.Context) ([]directory.Employee, error) {
	return []directory.Employee{}, nil
}

type candidateEmployeeDirectory struct {
	employees []directory.Employee
}

func (d candidateEmployeeDirectory) FindByID(ctx context.Context, id uint64) (*directory.Employee, error) {
	for _, employee := range d.employees {
		if employee.ID == id {
			copy := employee
			return &copy, nil
		}
	}
	return nil, errors.New("not found")
}

func (d candidateEmployeeDirectory) FindByEmail(ctx context.Context, email string) (*directory.Employee, error) {
	return nil, errors.New("not implemented")
}

func (d candidateEmployeeDirectory) FindByEmails(ctx context.Context, emails []string) ([]directory.Employee, error) {
	return []directory.Employee{}, nil
}

func (d candidateEmployeeDirectory) SearchEmployees(ctx context.Context, query string) ([]directory.Employee, error) {
	return append([]directory.Employee(nil), d.employees...), nil
}

func (d candidateEmployeeDirectory) ListEmployees(ctx context.Context) ([]directory.Employee, error) {
	return append([]directory.Employee(nil), d.employees...), nil
}

func newAccessLifecycleService(vuln *vulnmodel.Vulnerability, users map[uint64]*coremodel.User) (*Service, *accessLifecycleVulnRepo, *accessLifecycleEventRepo) {
	repo := &accessLifecycleVulnRepo{vuln: vuln}
	events := &accessLifecycleEventRepo{}
	svc := New(
		repo,
		nil,
		nil,
		nil,
		events,
		nil,
		nil,
		nil,
		nil,
		&accessLifecycleUserRepo{users: users},
		noopEmployeeDirectory{},
	)
	return svc, repo, events
}

func userWithEmployeeAndRoles(userID, employeeID uint64, roles ...string) *coremodel.User {
	var employeePtr *uint64
	if employeeID > 0 {
		id := employeeID
		employeePtr = &id
	}
	userRoles := make([]coremodel.Role, 0, len(roles))
	for _, role := range roles {
		userRoles = append(userRoles, coremodel.Role{
			Name:        role,
			Permissions: fallbackRolePermissions(role),
		})
	}
	return &coremodel.User{
		Base:       coremodel.Base{ID: userID},
		Email:      "user@example.com",
		Name:       "User",
		EmployeeID: employeePtr,
		Roles:      userRoles,
	}
}

func userWithEmployeeAndPermissions(userID, employeeID uint64, permissions ...string) *coremodel.User {
	var employeePtr *uint64
	if employeeID > 0 {
		id := employeeID
		employeePtr = &id
	}
	rolePermissions := make([]coremodel.Permission, 0, len(permissions))
	for _, permission := range permissions {
		rolePermissions = append(rolePermissions, coremodel.Permission{Name: permission})
	}
	return &coremodel.User{
		Base:       coremodel.Base{ID: userID},
		Email:      "user@example.com",
		Name:       "User",
		EmployeeID: employeePtr,
		Roles: []coremodel.Role{{
			Name:        "custom",
			Permissions: rolePermissions,
		}},
	}
}

func fallbackRolePermissions(role string) []coremodel.Permission {
	switch role {
	case "admin", "security", "security_operator":
		return []coremodel.Permission{
			{Name: permissionViewFindings},
			{Name: permissionManageFindings},
			{Name: permissionViewSensitive},
			{Name: permissionEditSensitive},
			{Name: permissionAuthorizeViewers},
			{Name: permissionAssignOwner},
			{Name: permissionRequestRetest},
			{Name: permissionCloseFindings},
		}
	default:
		return []coremodel.Permission{{Name: permissionViewFindings}}
	}
}

func TestCanViewVulnerabilityAndAvailableActionsByActorRole(t *testing.T) {
	ctx := context.Background()
	submitterUserID := uint64(10)
	ownerEmployeeID := uint64(20)
	fixerEmployeeID := uint64(30)
	strangerUserID := uint64(40)
	adminUserID := uint64(50)
	authorizedViewerUserID := uint64(60)
	authorizedViewerEmployeeID := uint64(61)
	editDetailsUserID := uint64(70)
	editDetailsEmployeeID := uint64(71)

	vuln := &vulnmodel.Vulnerability{
		Base:                        coremodel.Base{ID: 77},
		SubmitterUserID:             submitterUserID,
		OwnerEmployeeID:             &ownerEmployeeID,
		FixerEmployeeID:             &fixerEmployeeID,
		AuthorizedViewerEmployeeIDs: vulnmodel.UInt64Array{authorizedViewerEmployeeID},
		Status:                      vulnmodel.StatusActive,
		VerificationSteps:           "steps",
		POCText:                     "poc",
	}
	svc, _, _ := newAccessLifecycleService(vuln, map[uint64]*coremodel.User{
		submitterUserID: userWithEmployeeAndRoles(submitterUserID, 10),
		20:              userWithEmployeeAndRoles(20, ownerEmployeeID),
		30:              userWithEmployeeAndPermissions(30, fixerEmployeeID, permissionViewFindings, permissionRequestRetest),
		strangerUserID:  userWithEmployeeAndRoles(strangerUserID, 40),
		adminUserID:     userWithEmployeeAndRoles(adminUserID, 50, "admin"),
		editDetailsUserID: userWithEmployeeAndPermissions(
			editDetailsUserID,
			editDetailsEmployeeID,
			permissionViewFindings,
			permissionEditDetails,
		),
		authorizedViewerUserID: userWithEmployeeAndRoles(
			authorizedViewerUserID,
			authorizedViewerEmployeeID,
		),
	})

	for _, tc := range []struct {
		name            string
		userID          uint64
		wantView        bool
		wantContains    []string
		wantNotContains []string
	}{
		{
			name:            "submitter can view and edit sensitive details",
			userID:          submitterUserID,
			wantView:        true,
			wantContains:    []string{"view", "comment", actionViewSensitiveDetails, actionEditSensitiveDetails},
			wantNotContains: []string{"mark_fixed", actionRequestRetest},
		},
		{
			name:            "owner can view before fixed",
			userID:          20,
			wantView:        true,
			wantContains:    []string{"view"},
			wantNotContains: []string{"comment", "claim", actionRequestRetest},
		},
		{
			name:            "fixer can view and request retest",
			userID:          30,
			wantView:        true,
			wantContains:    []string{"view", "comment", "claim", actionRequestRetest},
			wantNotContains: []string{"mark_fixed", actionEditSensitiveDetails},
		},
		{
			name:            "privileged user can view and manage restricted actions",
			userID:          adminUserID,
			wantView:        true,
			wantContains:    []string{"view", "comment", "assign_owner", actionEditDetails, actionEditSensitiveDetails, actionAuthorizeViewers, "mark_false_positive", "mark_risk_accepted"},
			wantNotContains: []string{"claim"},
		},
		{
			name:            "edit details permission can view and edit detail fields",
			userID:          editDetailsUserID,
			wantView:        true,
			wantContains:    []string{"view", "comment", actionViewSensitiveDetails, actionEditDetails, "assign_owner", "edit_tags", actionEditSensitiveDetails},
			wantNotContains: []string{"claim", actionAuthorizeViewers},
		},
		{
			name:            "authorized viewer can view but cannot claim or comment",
			userID:          authorizedViewerUserID,
			wantView:        true,
			wantContains:    []string{"view"},
			wantNotContains: []string{"comment", "claim", actionRequestRetest, actionEditSensitiveDetails},
		},
		{
			name:            "unrelated user cannot view",
			userID:          strangerUserID,
			wantView:        false,
			wantContains:    []string{},
			wantNotContains: []string{"view", "comment", "claim"},
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			canView, err := svc.CanViewVulnerability(ctx, tc.userID, vuln.ID)
			require.NoError(t, err)
			require.Equal(t, tc.wantView, canView)

			actions, err := svc.AvailableActions(ctx, tc.userID, vuln)
			require.NoError(t, err)
			for _, want := range tc.wantContains {
				require.Contains(t, actions, want)
			}
			for _, notWant := range tc.wantNotContains {
				require.NotContains(t, actions, notWant)
			}
		})
	}
}

func TestPublicViewableVulnerabilityIsVisibleToAuthenticatedUserWithoutViewPermission(t *testing.T) {
	ctx := context.Background()
	userID := uint64(90)
	vuln := &vulnmodel.Vulnerability{
		Base:              coremodel.Base{ID: 78},
		SubmitterUserID:   10,
		Status:            vulnmodel.StatusActive,
		IsPublicViewable:  true,
		VerificationSteps: "steps",
		POCText:           "poc",
	}
	svc, _, _ := newAccessLifecycleService(vuln, map[uint64]*coremodel.User{
		userID: userWithEmployeeAndPermissions(userID, 91),
	})

	canView, err := svc.CanViewVulnerability(ctx, userID, vuln.ID)
	require.NoError(t, err)
	require.True(t, canView)

	actions, err := svc.AvailableActions(ctx, userID, vuln)
	require.NoError(t, err)
	require.Equal(t, []string{"view"}, actions)
}

func TestUpdatePublicVisibilityRequiresAuthorizePermission(t *testing.T) {
	ctx := context.Background()
	authorizedUserID := uint64(10)
	unauthorizedUserID := uint64(11)
	vuln := &vulnmodel.Vulnerability{
		Base:            coremodel.Base{ID: 79},
		SubmitterUserID: 20,
		Status:          vulnmodel.StatusActive,
	}
	svc, repo, events := newAccessLifecycleService(vuln, map[uint64]*coremodel.User{
		authorizedUserID:   userWithEmployeeAndPermissions(authorizedUserID, 100, permissionAuthorizeViewers),
		unauthorizedUserID: userWithEmployeeAndPermissions(unauthorizedUserID, 101, permissionViewFindings),
	})

	_, err := svc.UpdatePublicVisibility(ctx, vuln.ID, true, "Nope", &unauthorizedUserID)
	require.ErrorContains(t, err, "not authorized")

	updated, err := svc.UpdatePublicVisibility(ctx, vuln.ID, true, "Security", &authorizedUserID)
	require.NoError(t, err)
	require.True(t, updated.IsPublicViewable)
	require.Equal(t, true, repo.lastUpdateFields["is_public_viewable"])
	require.Len(t, events.events, 1)
	require.Equal(t, "public_visibility_updated", events.events[0].EventType)
	require.Equal(t, true, events.events[0].Metadata["is_public_viewable"])
}

func TestUpdatePublicVisibilityDoesNotUseRoleNameAuthorization(t *testing.T) {
	ctx := context.Background()
	adminUserID := uint64(10)
	vuln := &vulnmodel.Vulnerability{
		Base:            coremodel.Base{ID: 80},
		SubmitterUserID: 20,
		Status:          vulnmodel.StatusActive,
	}
	svc, _, _ := newAccessLifecycleService(vuln, map[uint64]*coremodel.User{
		adminUserID: {
			Base:  coremodel.Base{ID: adminUserID},
			Email: "admin@example.com",
			Name:  "Admin",
			Roles: []coremodel.Role{{Name: "admin"}},
		},
	})

	_, err := svc.UpdatePublicVisibility(ctx, vuln.ID, true, "Admin", &adminUserID)
	require.ErrorContains(t, err, "not authorized")
}

func TestSearchAuthorizedViewerCandidatesRequiresPermissionAndFiltersActiveEmployees(t *testing.T) {
	ctx := context.Background()
	authorizedUserID := uint64(10)
	unauthorizedUserID := uint64(11)

	employees := candidateEmployeeDirectory{employees: []directory.Employee{
		{ID: 1, Name: "Active One", Email: "active@example.com", Status: "active"},
		{ID: 2, Name: "Inactive One", Email: "inactive@example.com", Status: "inactive"},
		{ID: 3, Name: "Blank Status", Email: "blank@example.com"},
	}}
	users := map[uint64]*coremodel.User{
		authorizedUserID:   userWithEmployeeAndPermissions(authorizedUserID, 100, permissionAuthorizeViewers),
		unauthorizedUserID: userWithEmployeeAndPermissions(unauthorizedUserID, 101, permissionViewFindings),
	}

	svc := New(
		&accessLifecycleVulnRepo{vuln: &vulnmodel.Vulnerability{
			Base:   coremodel.Base{ID: 99},
			Status: vulnmodel.StatusActive,
		}},
		nil,
		nil,
		nil,
		&accessLifecycleEventRepo{},
		nil,
		nil,
		nil,
		nil,
		&accessLifecycleUserRepo{users: users},
		employees,
	)

	candidates, err := svc.SearchAuthorizedViewerCandidates(ctx, 99, "one", nil, &authorizedUserID)
	require.NoError(t, err)
	require.Len(t, candidates, 1)
	require.Equal(t, uint64(1), candidates[0].ID)

	_, err = svc.SearchAuthorizedViewerCandidates(ctx, 99, "one", nil, &unauthorizedUserID)
	require.ErrorContains(t, err, "not authorized")
}

func TestTransitionStatusSupportsRequestRetestFixedAndReopenFixed(t *testing.T) {
	ctx := context.Background()
	fixerEmployeeID := uint64(30)
	fixerUserID := uint64(3)
	securityUserID := uint64(4)

	vuln := &vulnmodel.Vulnerability{
		Base:            coremodel.Base{ID: 88},
		SubmitterUserID: 11,
		FixerEmployeeID: &fixerEmployeeID,
		Status:          vulnmodel.StatusActive,
	}
	svc, repo, events := newAccessLifecycleService(vuln, map[uint64]*coremodel.User{
		fixerUserID:    userWithEmployeeAndPermissions(fixerUserID, fixerEmployeeID, permissionViewFindings, permissionRequestRetest),
		securityUserID: userWithEmployeeAndRoles(securityUserID, 44, "security_operator"),
	})

	updated, err := svc.TransitionStatus(ctx, vuln.ID, vulnmodel.StatusRetesting, "request retest", "Fixer", &fixerUserID)
	require.NoError(t, err)
	require.Equal(t, vulnmodel.StatusRetesting, updated.Status)
	require.NotNil(t, updated.FixRequestedAt)
	require.Contains(t, repo.lastUpdateFields, "status")
	require.Contains(t, repo.lastUpdateFields, "fix_requested_at")
	require.Len(t, events.events, 1)
	require.Equal(t, "status_changed", events.events[0].EventType)
	require.Equal(t, "request retest", events.events[0].Message)
	require.Equal(t, "retesting", events.events[0].Metadata["status"])

	updated, err = svc.TransitionStatus(ctx, vuln.ID, vulnmodel.StatusFixed, "mark fixed", "Security", &securityUserID)
	require.NoError(t, err)
	require.Equal(t, vulnmodel.StatusFixed, updated.Status)
	require.NotNil(t, updated.FixedAt)
	require.Contains(t, repo.lastUpdateFields, "fixed_at")
	require.Len(t, events.events, 2)
	require.Equal(t, "fixed", events.events[1].Metadata["status"])

	updated, err = svc.TransitionStatus(ctx, vuln.ID, vulnmodel.StatusActive, "reopen", "Security", &securityUserID)
	require.NoError(t, err)
	require.Equal(t, vulnmodel.StatusActive, updated.Status)
	require.Nil(t, updated.FixedAt)
	require.Nil(t, updated.ClosedAt)
	require.Contains(t, repo.lastUpdateFields, "fixed_at")
	require.Contains(t, repo.lastUpdateFields, "closed_at")
	require.Len(t, events.events, 3)
	require.Equal(t, "active", events.events[2].Metadata["status"])
}

func TestTransitionStatusRejectsSubmitterWithoutMutateRights(t *testing.T) {
	ctx := context.Background()
	submitterUserID := uint64(10)
	vuln := &vulnmodel.Vulnerability{
		Base:            coremodel.Base{ID: 99},
		SubmitterUserID: submitterUserID,
		Status:          vulnmodel.StatusActive,
	}
	svc, _, _ := newAccessLifecycleService(vuln, map[uint64]*coremodel.User{
		submitterUserID: userWithEmployeeAndRoles(submitterUserID, 10),
	})

	_, err := svc.TransitionStatus(ctx, vuln.ID, vulnmodel.StatusRetesting, "request retest", "Submitter", &submitterUserID)
	require.Error(t, err)
	require.Contains(t, err.Error(), "not authorized to change vulnerability status")
}
