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 updateMetaVulnRepo struct {
	vuln             *vulnmodel.Vulnerability
	lastUpdateFields map[string]interface{}
}

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

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

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

func (r *updateMetaVulnRepo) 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 *updateMetaVulnRepo) Update(ctx context.Context, vuln *vulnmodel.Vulnerability) error {
	return nil
}

func (r *updateMetaVulnRepo) 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
		switch k {
		case "title":
			if text, ok := v.(string); ok {
				r.vuln.Title = text
			}
		case "description":
			if text, ok := v.(string); ok {
				r.vuln.Description = text
			}
		case "verification_steps":
			if text, ok := v.(string); ok {
				r.vuln.VerificationSteps = text
			}
		case "poc_text":
			if text, ok := v.(string); ok {
				r.vuln.POCText = text
			}
		case "tags":
			if tags, ok := v.(vulnmodel.StringArray); ok {
				r.vuln.Tags = tags
			}
		case "owner_employee_id":
			if id, ok := v.(*uint64); ok {
				r.vuln.OwnerEmployeeID = id
			}
		}
	}
	return nil
}

type updateMetaEventRepo struct {
	events []vulnmodel.VulnerabilityEvent
}

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

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

type updateMetaUserRepo struct {
	user *coremodel.User
}

func (r *updateMetaUserRepo) FindByID(ctx context.Context, id uint64) (*coremodel.User, error) {
	if r.user == nil || r.user.ID != id {
		return nil, errors.New("not found")
	}
	copy := *r.user
	return &copy, nil
}

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

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

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

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

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

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

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

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

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

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

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

func (r *updateMetaUserRepo) FindByEmailOrExternalID(ctx context.Context, identifier string) (*coremodel.User, error) {
	return nil, errors.New("not implemented")
}

type noopEmployeeRepo struct{}

func (noopEmployeeRepo) FindByID(ctx context.Context, id uint64) (*directory.Employee, error) {
	return nil, nil
}
func (noopEmployeeRepo) FindByEmail(ctx context.Context, email string) (*directory.Employee, error) {
	return nil, nil
}
func (noopEmployeeRepo) FindByEmails(ctx context.Context, emails []string) ([]directory.Employee, error) {
	return []directory.Employee{}, nil
}
func (noopEmployeeRepo) SearchEmployees(ctx context.Context, query string) ([]directory.Employee, error) {
	return []directory.Employee{}, nil
}
func (noopEmployeeRepo) ListEmployees(ctx context.Context) ([]directory.Employee, error) {
	return []directory.Employee{}, nil
}

func TestUpdateMetaPersistsVerificationStepsAndPOCText(t *testing.T) {
	ctx := context.Background()
	vulnID := uint64(77)
	actorID := uint64(5)
	repo := &updateMetaVulnRepo{
		vuln: &vulnmodel.Vulnerability{
			Base:              coremodel.Base{ID: vulnID},
			SubmitterUserID:   5,
			VerificationSteps: "old steps",
			POCText:           "old poc",
		},
	}
	userRepo := &updateMetaUserRepo{
		user: &coremodel.User{
			Base:       coremodel.Base{ID: actorID},
			Email:      "admin@example.com",
			EmployeeID: func() *uint64 { id := uint64(9); return &id }(),
			Roles:      []coremodel.Role{{Name: "admin"}},
		},
	}
	events := &updateMetaEventRepo{}
	svc := New(repo, nil, nil, nil, events, nil, nil, nil, nil, userRepo, noopEmployeeRepo{})

	verificationSteps := "new steps"
	pocText := "new poc"
	updated, err := svc.UpdateMeta(ctx, vulnID, nil, nil, &verificationSteps, &pocText, "Admin", &actorID)
	require.NoError(t, err)
	require.NotNil(t, updated)
	require.Equal(t, "new steps", updated.VerificationSteps)
	require.Equal(t, "new poc", updated.POCText)
	require.Contains(t, repo.lastUpdateFields, "verification_steps")
	require.Contains(t, repo.lastUpdateFields, "poc_text")
	require.Len(t, events.events, 2)
	require.ElementsMatch(t, []string{"verification_steps_updated", "poc_updated"}, []string{events.events[0].EventType, events.events[1].EventType})
}

func TestUpdateMetaDoesNotEmitTagsEventWhenTagsUnchanged(t *testing.T) {
	ctx := context.Background()
	vulnID := uint64(78)
	actorID := uint64(6)
	repo := &updateMetaVulnRepo{
		vuln: &vulnmodel.Vulnerability{
			Base:            coremodel.Base{ID: vulnID},
			SubmitterUserID: 6,
			Tags:            vulnmodel.StringArray{},
		},
	}
	userRepo := &updateMetaUserRepo{
		user: &coremodel.User{
			Base:  coremodel.Base{ID: actorID},
			Email: "admin@example.com",
			Roles: []coremodel.Role{{Name: "admin"}},
		},
	}
	events := &updateMetaEventRepo{}
	svc := New(repo, nil, nil, nil, events, nil, nil, nil, nil, userRepo, noopEmployeeRepo{})

	verificationSteps := "updated steps"
	updated, err := svc.UpdateMeta(ctx, vulnID, nil, []string{}, &verificationSteps, nil, "Admin", &actorID)
	require.NoError(t, err)
	require.NotNil(t, updated)
	require.Equal(t, "updated steps", updated.VerificationSteps)
	require.NotContains(t, repo.lastUpdateFields, "tags")
	require.Len(t, events.events, 1)
	require.Equal(t, "verification_steps_updated", events.events[0].EventType)
}

func TestUpdateVulnerabilityEmitsSingleDetailsUpdatedEvent(t *testing.T) {
	ctx := context.Background()
	vulnID := uint64(79)
	actorID := uint64(7)
	newOwnerID := uint64(99)
	repo := &updateMetaVulnRepo{
		vuln: &vulnmodel.Vulnerability{
			Base:                coremodel.Base{ID: vulnID},
			SubmitterUserID:     88,
			Title:               "Old title",
			Description:         "Old description",
			VerificationSteps:   "old steps",
			POCText:             "old poc",
			Tags:                vulnmodel.StringArray{"legacy"},
			SeverityLevel:       vulnmodel.SeverityMedium,
			Status:              vulnmodel.StatusActive,
			SLADeadline:         time.Now().Add(4 * time.Hour).UTC(),
			HasPOCVerification:  false,
			IsThirdParty:        false,
			AffectedAssetsCount: 1,
		},
	}
	userRepo := &updateMetaUserRepo{
		user: &coremodel.User{
			Base:       coremodel.Base{ID: actorID},
			Email:      "editor@example.com",
			EmployeeID: func() *uint64 { id := uint64(9); return &id }(),
			Roles: []coremodel.Role{{
				Name: "custom",
				Permissions: []coremodel.Permission{
					{Name: permissionViewFindings},
					{Name: permissionEditDetails},
				},
			}},
		},
	}
	events := &updateMetaEventRepo{}
	svc := New(repo, nil, nil, nil, events, nil, nil, nil, nil, userRepo, noopEmployeeRepo{})

	title := "New title"
	description := "New description"
	verificationSteps := "new steps"
	pocText := "new poc"
	tags := []string{"critical-path", "customer-facing"}
	updated, err := svc.UpdateVulnerability(ctx, vulnID, UpdateVulnerabilityInput{
		Title:              &title,
		Description:        &description,
		VerificationSteps:  &verificationSteps,
		POCText:            &pocText,
		Tags:               &tags,
		OwnerEmployeeID:    &newOwnerID,
		OwnerEmployeeIDSet: true,
	}, "Editor", &actorID)
	require.NoError(t, err)
	require.NotNil(t, updated)
	require.Equal(t, "New title", updated.Title)
	require.Equal(t, "New description", updated.Description)
	require.Equal(t, "new steps", updated.VerificationSteps)
	require.Equal(t, "new poc", updated.POCText)
	require.Contains(t, repo.lastUpdateFields, "title")
	require.Contains(t, repo.lastUpdateFields, "description")
	require.Contains(t, repo.lastUpdateFields, "verification_steps")
	require.Contains(t, repo.lastUpdateFields, "poc_text")
	require.Contains(t, repo.lastUpdateFields, "tags")
	require.Contains(t, repo.lastUpdateFields, "owner_employee_id")
	require.Len(t, events.events, 1)
	require.Equal(t, "details_updated", events.events[0].EventType)
	changedFields, ok := events.events[0].Metadata["changed_fields"].([]string)
	require.True(t, ok)
	require.ElementsMatch(t, []string{
		"title",
		"description",
		"verification_steps",
		"poc_text",
		"tags",
		"owner_employee_id",
	}, changedFields)
}

func previousValuesTestActor(actorID uint64) *updateMetaUserRepo {
	return &updateMetaUserRepo{
		user: &coremodel.User{
			Base:       coremodel.Base{ID: actorID},
			Email:      "editor@example.com",
			EmployeeID: func() *uint64 { id := uint64(9); return &id }(),
			Roles: []coremodel.Role{{
				Name: "custom",
				Permissions: []coremodel.Permission{
					{Name: permissionViewFindings},
					{Name: permissionEditDetails},
				},
			}},
		},
	}
}

// P1-T7 ①：改普通字段 → details_updated 事件 meta 的 previous_values 含旧值
func TestUpdateVulnerabilityRecordsPreviousValuesForNormalFields(t *testing.T) {
	ctx := context.Background()
	vulnID := uint64(80)
	actorID := uint64(7)
	repo := &updateMetaVulnRepo{
		vuln: &vulnmodel.Vulnerability{
			Base:            coremodel.Base{ID: vulnID},
			SubmitterUserID: 88,
			Title:           "Old title",
			SeverityLevel:   vulnmodel.SeverityMedium,
			Status:          vulnmodel.StatusActive,
			SLADeadline:     time.Now().Add(4 * time.Hour).UTC(),
		},
	}
	events := &updateMetaEventRepo{}
	svc := New(repo, nil, nil, nil, events, nil, nil, nil, nil, previousValuesTestActor(actorID), noopEmployeeRepo{})

	title := "New title"
	severity := vulnmodel.SeverityHigh
	_, err := svc.UpdateVulnerability(ctx, vulnID, UpdateVulnerabilityInput{
		Title:         &title,
		SeverityLevel: &severity,
	}, "Editor", &actorID)
	require.NoError(t, err)
	require.Len(t, events.events, 1)
	require.Equal(t, "details_updated", events.events[0].EventType)
	prev, ok := events.events[0].Metadata["previous_values"].(map[string]interface{})
	require.True(t, ok, "previous_values should be a map")
	require.Equal(t, "Old title", prev["title"])
	require.Equal(t, vulnmodel.SeverityMedium, prev["severity_level"])
}

// P1-T7 ②：人类路径改敏感字段 → previous_values 不含 poc_text/verification_steps/has_poc_verification
func TestUpdateVulnerabilityExcludesSensitiveFieldsFromPreviousValues(t *testing.T) {
	ctx := context.Background()
	vulnID := uint64(81)
	actorID := uint64(7)
	repo := &updateMetaVulnRepo{
		vuln: &vulnmodel.Vulnerability{
			Base:               coremodel.Base{ID: vulnID},
			SubmitterUserID:    88,
			Title:              "Old title",
			SeverityLevel:      vulnmodel.SeverityMedium,
			Status:             vulnmodel.StatusActive,
			SLADeadline:        time.Now().Add(4 * time.Hour).UTC(),
			VerificationSteps:  "old steps",
			POCText:            "old poc",
			HasPOCVerification: false,
		},
	}
	events := &updateMetaEventRepo{}
	svc := New(repo, nil, nil, nil, events, nil, nil, nil, nil, previousValuesTestActor(actorID), noopEmployeeRepo{})

	title := "New title"
	verificationSteps := "new steps"
	pocText := "new poc"
	hasPOC := true
	_, err := svc.UpdateVulnerability(ctx, vulnID, UpdateVulnerabilityInput{
		Title:              &title,
		VerificationSteps:  &verificationSteps,
		POCText:            &pocText,
		HasPOCVerification: &hasPOC,
	}, "Editor", &actorID)
	require.NoError(t, err)
	require.Len(t, events.events, 1)
	prev, ok := events.events[0].Metadata["previous_values"].(map[string]interface{})
	require.True(t, ok, "previous_values should be a map")
	require.Equal(t, "Old title", prev["title"])
	require.NotContains(t, prev, "poc_text")
	require.NotContains(t, prev, "verification_steps")
	require.NotContains(t, prev, "has_poc_verification")
	// 敏感字段本身仍在 changed_fields 里（变更事实可见），只是不记旧值
	changedFields, ok := events.events[0].Metadata["changed_fields"].([]string)
	require.True(t, ok)
	require.Contains(t, changedFields, "poc_text")
}

// P1-T7 ③：未变更字段不出现在 previous_values
func TestUpdateVulnerabilityOmitsUnchangedFieldsFromPreviousValues(t *testing.T) {
	ctx := context.Background()
	vulnID := uint64(82)
	actorID := uint64(7)
	repo := &updateMetaVulnRepo{
		vuln: &vulnmodel.Vulnerability{
			Base:            coremodel.Base{ID: vulnID},
			SubmitterUserID: 88,
			Title:           "Same title",
			Description:     "Old description",
			SeverityLevel:   vulnmodel.SeverityMedium,
			Status:          vulnmodel.StatusActive,
			SLADeadline:     time.Now().Add(4 * time.Hour).UTC(),
		},
	}
	events := &updateMetaEventRepo{}
	svc := New(repo, nil, nil, nil, events, nil, nil, nil, nil, previousValuesTestActor(actorID), noopEmployeeRepo{})

	sameTitle := "Same title"
	description := "New description"
	_, err := svc.UpdateVulnerability(ctx, vulnID, UpdateVulnerabilityInput{
		Title:       &sameTitle,
		Description: &description,
	}, "Editor", &actorID)
	require.NoError(t, err)
	require.Len(t, events.events, 1)
	prev, ok := events.events[0].Metadata["previous_values"].(map[string]interface{})
	require.True(t, ok, "previous_values should be a map")
	require.Equal(t, "Old description", prev["description"])
	require.NotContains(t, prev, "title")
	require.NotContains(t, prev, "severity_level")
}
