package handler

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/require"
	"gorm.io/gorm"

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

type publicIDLookupVulnRepo struct {
	vulnsByPublicID map[string]*vulnmodel.Vulnerability
	vulnsByID       map[uint64]*vulnmodel.Vulnerability
	publicLookups   []string
	idLookups       []uint64
	updateFields    []struct {
		id     uint64
		fields map[string]interface{}
	}
}

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

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

func (r *publicIDLookupVulnRepo) FindByID(ctx context.Context, id uint64) (*vulnmodel.Vulnerability, error) {
	r.idLookups = append(r.idLookups, id)
	if vuln, ok := r.vulnsByID[id]; ok && vuln != nil {
		copy := *vuln
		return &copy, nil
	}
	return nil, gorm.ErrRecordNotFound
}

func (r *publicIDLookupVulnRepo) FindByPublicID(ctx context.Context, publicID string) (*vulnmodel.Vulnerability, error) {
	publicID = strings.TrimSpace(publicID)
	r.publicLookups = append(r.publicLookups, publicID)
	if vuln, ok := r.vulnsByPublicID[publicID]; ok && vuln != nil {
		copy := *vuln
		return &copy, nil
	}
	return nil, gorm.ErrRecordNotFound
}

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

func (r *publicIDLookupVulnRepo) UpdateFields(ctx context.Context, id uint64, fields map[string]interface{}) error {
	r.updateFields = append(r.updateFields, struct {
		id     uint64
		fields map[string]interface{}
	}{id: id, fields: fields})
	vuln, ok := r.vulnsByID[id]
	if !ok || vuln == nil {
		return gorm.ErrRecordNotFound
	}
	if status, ok := fields["status"].(vulnmodel.VulnerabilityStatus); ok {
		vuln.Status = status
	}
	if status, ok := fields["status"].(string); ok {
		vuln.Status = vulnmodel.VulnerabilityStatus(status)
	}
	if ts, ok := fields["fix_requested_at"].(time.Time); ok {
		vuln.FixRequestedAt = &ts
	}
	if ts, ok := fields["fixed_at"].(time.Time); ok {
		vuln.FixedAt = &ts
	}
	if _, ok := fields["fixed_at"]; ok && fields["fixed_at"] == nil {
		vuln.FixedAt = nil
	}
	if ts, ok := fields["closed_at"].(time.Time); ok {
		vuln.ClosedAt = &ts
	}
	if _, ok := fields["closed_at"]; ok && fields["closed_at"] == nil {
		vuln.ClosedAt = nil
	}
	if isPublicViewable, ok := fields["is_public_viewable"].(bool); ok {
		vuln.IsPublicViewable = isPublicViewable
	}
	return nil
}

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

func (r *publicIDLookupUserRepo) 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, gorm.ErrRecordNotFound
}

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

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

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

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

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

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

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

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

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

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

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

func newPublicIDLookupHandler(t *testing.T, repo *publicIDLookupVulnRepo, users map[uint64]*coremodel.User) *gin.Engine {
	t.Helper()
	gin.SetMode(gin.TestMode)

	svc := vulnservice.New(repo, nil, nil, nil, nil, nil, nil, nil, nil, &publicIDLookupUserRepo{users: users}, nil)
	h := &Handler{service: svc}

	engine := gin.New()
	engine.Use(func(c *gin.Context) {
		c.Set("current_user", &jwt.Claims{
			UserID: 1,
			Email:  "actor@example.com",
		})
		c.Next()
	})
	h.RegisterRoutes(engine.Group("/api/v1"))
	return engine
}

func newPublicIDLookupCandidateHandler(t *testing.T, repo *publicIDLookupVulnRepo, users map[uint64]*coremodel.User) *gin.Engine {
	t.Helper()
	gin.SetMode(gin.TestMode)

	svc := vulnservice.New(repo, nil, nil, nil, nil, nil, nil, nil, nil, &publicIDLookupUserRepo{users: users}, nil)
	h := &Handler{service: svc}

	engine := gin.New()
	engine.Use(func(c *gin.Context) {
		c.Set("current_user", &jwt.Claims{
			UserID: 1,
			Email:  "actor@example.com",
		})
		c.Next()
	})
	h.RegisterAuthorizedViewerCandidateRoutes(engine.Group("/api/v1"))
	return engine
}

func TestVulnerabilityRoutesResolvePublicIDBeforeInternalID(t *testing.T) {
	repo := &publicIDLookupVulnRepo{
		vulnsByPublicID: map[string]*vulnmodel.Vulnerability{
			"vul-26-0001": {
				Base:            coremodel.Base{ID: 77},
				PublicID:        "vul-26-0001",
				SubmitterUserID: 1,
				Status:          vulnmodel.StatusActive,
			},
		},
		vulnsByID: map[uint64]*vulnmodel.Vulnerability{
			77: {
				Base:            coremodel.Base{ID: 77},
				PublicID:        "vul-26-0001",
				SubmitterUserID: 1,
				Status:          vulnmodel.StatusActive,
			},
		},
	}
	engine := newPublicIDLookupHandler(t, repo, map[uint64]*coremodel.User{
		1: {
			Base:  coremodel.Base{ID: 1},
			Email: "actor@example.com",
			Roles: []coremodel.Role{{Name: "admin"}},
		},
	})

	t.Run("get uses public id", func(t *testing.T) {
		req := httptest.NewRequest(http.MethodGet, "/api/v1/vulnerabilities/vul-26-0001", nil)
		rr := httptest.NewRecorder()

		engine.ServeHTTP(rr, req)

		require.Equal(t, http.StatusOK, rr.Code)
		resp := decodeHandlerAPIResponse(t, rr.Body.Bytes())
		require.Equal(t, 0, resp.Code)
		require.Equal(t, []string{"vul-26-0001"}, repo.publicLookups)
		require.Equal(t, []uint64{77}, repo.idLookups)
	})

	t.Run("numeric path does not fall back to internal id", func(t *testing.T) {
		repo.publicLookups = nil
		repo.idLookups = nil

		req := httptest.NewRequest(http.MethodGet, "/api/v1/vulnerabilities/77", nil)
		rr := httptest.NewRecorder()

		engine.ServeHTTP(rr, req)

		require.Equal(t, http.StatusOK, rr.Code)
		resp := decodeHandlerAPIResponse(t, rr.Body.Bytes())
		require.Equal(t, http.StatusNotFound, resp.Code)
		require.Equal(t, []string{"77"}, repo.publicLookups)
		require.Empty(t, repo.idLookups)
	})
}

func TestTransitionStatusUsesInternalIDAfterPublicIDLookup(t *testing.T) {
	repo := &publicIDLookupVulnRepo{
		vulnsByPublicID: map[string]*vulnmodel.Vulnerability{
			"vul-26-0002": {
				Base:            coremodel.Base{ID: 88},
				PublicID:        "vul-26-0002",
				SubmitterUserID: 1,
				FixerEmployeeID: func() *uint64 { id := uint64(900); return &id }(),
				Status:          vulnmodel.StatusActive,
			},
		},
		vulnsByID: map[uint64]*vulnmodel.Vulnerability{
			88: {
				Base:            coremodel.Base{ID: 88},
				PublicID:        "vul-26-0002",
				SubmitterUserID: 1,
				FixerEmployeeID: func() *uint64 { id := uint64(900); return &id }(),
				Status:          vulnmodel.StatusActive,
			},
		},
	}
	engine := newPublicIDLookupHandler(t, repo, map[uint64]*coremodel.User{
		1: {
			Base:       coremodel.Base{ID: 1},
			Email:      "actor@example.com",
			EmployeeID: func() *uint64 { id := uint64(900); return &id }(),
			Roles: []coremodel.Role{{
				Name: "security_operator",
				Permissions: []coremodel.Permission{
					{Name: "vuln:request_retest"},
				},
			}},
		},
	})

	body, err := json.Marshal(map[string]any{
		"status":  string(vulnmodel.StatusRetesting),
		"message": "request retest",
	})
	require.NoError(t, err)

	req := httptest.NewRequest(http.MethodPost, "/api/v1/vulnerabilities/vul-26-0002/transition", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	rr := httptest.NewRecorder()

	engine.ServeHTTP(rr, req)

	require.Equal(t, http.StatusOK, rr.Code)
	resp := decodeHandlerAPIResponse(t, rr.Body.Bytes())
	require.Equal(t, 0, resp.Code)
	require.Equal(t, []string{"vul-26-0002"}, repo.publicLookups)
	require.Equal(t, []uint64{88, 88}, repo.idLookups)
	require.Len(t, repo.updateFields, 1)
	require.Equal(t, uint64(88), repo.updateFields[0].id)
	require.Equal(t, vulnmodel.StatusRetesting, repo.vulnsByID[88].Status)
}

func TestSearchAuthorizedViewerCandidatesDoesNotProbePublicIDWhenUnauthorized(t *testing.T) {
	repo := &publicIDLookupVulnRepo{
		vulnsByPublicID: map[string]*vulnmodel.Vulnerability{
			"vul-26-0003": {
				Base:     coremodel.Base{ID: 99},
				PublicID: "vul-26-0003",
				Status:   vulnmodel.StatusActive,
			},
		},
		vulnsByID: map[uint64]*vulnmodel.Vulnerability{
			99: {
				Base:     coremodel.Base{ID: 99},
				PublicID: "vul-26-0003",
				Status:   vulnmodel.StatusActive,
			},
		},
	}
	engine := newPublicIDLookupCandidateHandler(t, repo, map[uint64]*coremodel.User{
		1: {
			Base:  coremodel.Base{ID: 1},
			Email: "actor@example.com",
			Roles: []coremodel.Role{{Permissions: []coremodel.Permission{{Name: "vuln:view_findings"}}}},
		},
	})

	for _, path := range []string{
		"/api/v1/vulnerabilities/vul-26-0003/viewers/candidates?q=alice",
		"/api/v1/vulnerabilities/missing-public-id/viewers/candidates?q=alice",
	} {
		req := httptest.NewRequest(http.MethodGet, path, nil)
		rr := httptest.NewRecorder()

		engine.ServeHTTP(rr, req)

		require.Equal(t, http.StatusOK, rr.Code)
		resp := decodeHandlerAPIResponse(t, rr.Body.Bytes())
		require.Equal(t, http.StatusForbidden, resp.Code)
		require.Contains(t, resp.Message, "not authorized")
	}
	require.Empty(t, repo.publicLookups)
	require.Empty(t, repo.idLookups)
}

func TestUpdatePublicVisibilityUsesPublicIDAndRequiresPermission(t *testing.T) {
	repo := &publicIDLookupVulnRepo{
		vulnsByPublicID: map[string]*vulnmodel.Vulnerability{
			"vul-26-0004": {
				Base:            coremodel.Base{ID: 101},
				PublicID:        "vul-26-0004",
				SubmitterUserID: 2,
				Status:          vulnmodel.StatusActive,
			},
		},
		vulnsByID: map[uint64]*vulnmodel.Vulnerability{
			101: {
				Base:            coremodel.Base{ID: 101},
				PublicID:        "vul-26-0004",
				SubmitterUserID: 2,
				Status:          vulnmodel.StatusActive,
			},
		},
	}
	engine := newPublicIDLookupHandler(t, repo, map[uint64]*coremodel.User{
		1: {
			Base:  coremodel.Base{ID: 1},
			Email: "actor@example.com",
			Roles: []coremodel.Role{{Permissions: []coremodel.Permission{{Name: "vuln:authorize_viewers"}}}},
		},
	})

	body := bytes.NewBufferString(`{"is_public_viewable":true}`)
	req := httptest.NewRequest(http.MethodPost, "/api/v1/vulnerabilities/vul-26-0004/visibility", body)
	req.Header.Set("Content-Type", "application/json")
	rr := httptest.NewRecorder()

	engine.ServeHTTP(rr, req)

	require.Equal(t, http.StatusOK, rr.Code)
	resp := decodeHandlerAPIResponse(t, rr.Body.Bytes())
	require.Equal(t, 0, resp.Code)
	require.Equal(t, []string{"vul-26-0004"}, repo.publicLookups)
	require.Contains(t, repo.idLookups, uint64(101))
	require.Len(t, repo.updateFields, 1)
	require.Equal(t, true, repo.updateFields[0].fields["is_public_viewable"])
	require.True(t, repo.vulnsByID[101].IsPublicViewable)
}

func TestGetPublicViewableVulnerabilitySanitizesSensitiveFields(t *testing.T) {
	repo := &publicIDLookupVulnRepo{
		vulnsByPublicID: map[string]*vulnmodel.Vulnerability{
			"vul-26-0005": {
				Base:                        coremodel.Base{ID: 102},
				PublicID:                    "vul-26-0005",
				SubmitterUserID:             2,
				Status:                      vulnmodel.StatusActive,
				IsPublicViewable:            true,
				VerificationSteps:           "secret steps",
				POCText:                     "secret poc",
				AuthorizedViewerEmployeeIDs: vulnmodel.UInt64Array{88},
			},
		},
		vulnsByID: map[uint64]*vulnmodel.Vulnerability{
			102: {
				Base:                        coremodel.Base{ID: 102},
				PublicID:                    "vul-26-0005",
				SubmitterUserID:             2,
				Status:                      vulnmodel.StatusActive,
				IsPublicViewable:            true,
				VerificationSteps:           "secret steps",
				POCText:                     "secret poc",
				AuthorizedViewerEmployeeIDs: vulnmodel.UInt64Array{88},
			},
		},
	}
	engine := newPublicIDLookupHandler(t, repo, map[uint64]*coremodel.User{
		1: {
			Base:  coremodel.Base{ID: 1},
			Email: "actor@example.com",
			Roles: []coremodel.Role{{Permissions: []coremodel.Permission{}}},
		},
	})

	req := httptest.NewRequest(http.MethodGet, "/api/v1/vulnerabilities/vul-26-0005", nil)
	rr := httptest.NewRecorder()

	engine.ServeHTTP(rr, req)

	require.Equal(t, http.StatusOK, rr.Code)
	resp := decodeHandlerAPIResponse(t, rr.Body.Bytes())
	require.Equal(t, 0, resp.Code)

	var payload struct {
		Vulnerability vulnmodel.Vulnerability `json:"vulnerability"`
		Actions       []string                `json:"actions"`
	}
	require.NoError(t, json.Unmarshal(resp.Data, &payload))
	require.True(t, payload.Vulnerability.IsPublicViewable)
	require.Empty(t, payload.Vulnerability.VerificationSteps)
	require.Empty(t, payload.Vulnerability.POCText)
	require.Empty(t, payload.Vulnerability.AuthorizedViewerEmployeeIDs)
	require.Equal(t, []string{"view"}, payload.Actions)
}

func decodeHandlerAPIResponse(t *testing.T, body []byte) struct {
	Code    int             `json:"code"`
	Message string          `json:"message"`
	Data    json.RawMessage `json:"data"`
} {
	t.Helper()
	var resp struct {
		Code    int             `json:"code"`
		Message string          `json:"message"`
		Data    json.RawMessage `json:"data"`
	}
	require.NoError(t, json.Unmarshal(body, &resp))
	return resp
}
