package router

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

	"github.com/gin-gonic/gin"

	authhandler "security-center-backend/internal/handler/auth"
	healthhandler "security-center-backend/internal/handler/health"
	coremodel "security-center-backend/internal/model"
	caasmhandler "security-center-backend/internal/modules/caasm/handler"
	caasmmodel "security-center-backend/internal/modules/caasm/model"
	"security-center-backend/internal/modules/caasm/repository"
	caasmservice "security-center-backend/internal/modules/caasm/service"
	jwtpkg "security-center-backend/internal/service/jwt"
)

func TestCAASMRoutesSplitViewManageAndIngestPermissions(t *testing.T) {
	gin.SetMode(gin.TestMode)
	t.Setenv("CAASM_INGEST_API_KEY", "")

	engine := gin.New()
	RegisterRoutes(engine, Dependencies{
		Auth:              authhandler.NewHandler(nil, nil, nil, nil),
		Health:            healthhandler.NewHandler(),
		CAASM:             newTestCAASMHandler(caasmhandler.NewWebhookHandler(nil, nil, nil, nil)),
		AuthMiddleware:    allowAuth,
		RequirePermission: requireTestPermission,
	})

	tests := []struct {
		name        string
		method      string
		path        string
		permissions string
		wantHTTP    int
		wantCode    int
	}{
		{
			name:        "view permission can access assets list",
			method:      http.MethodGet,
			path:        "/api/v1/caasm/assets",
			permissions: "caasm:view_assets",
			wantHTTP:    http.StatusOK,
			wantCode:    0,
		},
		{
			name:        "view permission can access asset detail",
			method:      http.MethodGet,
			path:        "/api/v1/caasm/assets/1",
			permissions: "caasm:view_assets",
			wantHTTP:    http.StatusOK,
			wantCode:    0,
		},
		{
			name:        "view permission can access domains list",
			method:      http.MethodGet,
			path:        "/api/v1/caasm/domains",
			permissions: "caasm:view_assets",
			wantHTTP:    http.StatusOK,
			wantCode:    0,
		},
		{
			name:        "view permission can access ports list",
			method:      http.MethodGet,
			path:        "/api/v1/caasm/ports",
			permissions: "caasm:view_assets",
			wantHTTP:    http.StatusOK,
			wantCode:    0,
		},
		{
			name:        "view permission can access ip assets list",
			method:      http.MethodGet,
			path:        "/api/v1/caasm/ip-assets",
			permissions: "caasm:view_assets",
			wantHTTP:    http.StatusOK,
			wantCode:    0,
		},
		{
			name:        "view permission can access sync jobs list",
			method:      http.MethodGet,
			path:        "/api/v1/caasm/sync-jobs",
			permissions: "caasm:view_assets",
			wantHTTP:    http.StatusOK,
			wantCode:    0,
		},
		{
			name:        "view permission can access sync job detail",
			method:      http.MethodGet,
			path:        "/api/v1/caasm/sync-jobs/1",
			permissions: "caasm:view_assets",
			wantHTTP:    http.StatusOK,
			wantCode:    0,
		},
		{
			name:        "view permission can access sync job events",
			method:      http.MethodGet,
			path:        "/api/v1/caasm/sync-jobs/1/events",
			permissions: "caasm:view_assets",
			wantHTTP:    http.StatusOK,
			wantCode:    0,
		},
		{
			name:        "view permission cannot update asset tags",
			method:      http.MethodPut,
			path:        "/api/v1/caasm/assets/1/tags",
			permissions: "caasm:view_assets",
			wantHTTP:    http.StatusForbidden,
		},
		{
			name:        "view permission cannot start runtime",
			method:      http.MethodPost,
			path:        "/api/v1/caasm/runtime/start",
			permissions: "caasm:view_assets",
			wantHTTP:    http.StatusForbidden,
		},
		{
			name:        "view permission cannot enable kill switch",
			method:      http.MethodPost,
			path:        "/api/v1/caasm/runtime/kill-switch/enable",
			permissions: "caasm:view_assets",
			wantHTTP:    http.StatusForbidden,
		},
		{
			name:        "view permission cannot trigger cloud account sync",
			method:      http.MethodPost,
			path:        "/api/v1/caasm/cloud-accounts/1/sync",
			permissions: "caasm:view_assets",
			wantHTTP:    http.StatusForbidden,
		},
		{
			name:        "view permission cannot stop sync job",
			method:      http.MethodPost,
			path:        "/api/v1/caasm/sync-jobs/1/stop",
			permissions: "caasm:view_assets",
			wantHTTP:    http.StatusForbidden,
		},
		{
			name:        "manage permission reaches asset tag handler",
			method:      http.MethodPut,
			path:        "/api/v1/caasm/assets/1/tags",
			permissions: "caasm:manage_assets",
			wantHTTP:    http.StatusOK,
			wantCode:    http.StatusBadRequest,
		},
		{
			name:        "manage permission reaches sync stop handler",
			method:      http.MethodPost,
			path:        "/api/v1/caasm/sync-jobs/1/stop",
			permissions: "caasm:manage_assets",
			wantHTTP:    http.StatusOK,
			wantCode:    0,
		},
		{
			name:        "ingest route does not require view permission but fails closed without configured key",
			method:      http.MethodPost,
			path:        "/api/v1/caasm/ingest",
			permissions: "",
			wantHTTP:    http.StatusOK,
			wantCode:    http.StatusUnauthorized,
		},
		{
			name:        "start sync job fails closed without configured key",
			method:      http.MethodPost,
			path:        "/api/v1/caasm/sync-jobs/1/start",
			permissions: "",
			wantHTTP:    http.StatusOK,
			wantCode:    http.StatusUnauthorized,
		},
		{
			name:        "report sync job event fails closed without configured key",
			method:      http.MethodPost,
			path:        "/api/v1/caasm/sync-jobs/1/events",
			permissions: "",
			wantHTTP:    http.StatusOK,
			wantCode:    http.StatusUnauthorized,
		},
		{
			name:        "fail sync job does not require view permission but fails closed without configured key",
			method:      http.MethodPost,
			path:        "/api/v1/caasm/sync-jobs/1/fail",
			permissions: "",
			wantHTTP:    http.StatusOK,
			wantCode:    http.StatusUnauthorized,
		},
		{
			name:        "webhook endpoint is managed until provider signature guard exists",
			method:      http.MethodPost,
			path:        "/api/v1/caasm/webhooks/aws",
			permissions: "caasm:view_assets",
			wantHTTP:    http.StatusForbidden,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			req := httptest.NewRequest(tt.method, tt.path, nil)
			req.Header.Set("X-Test-Permissions", tt.permissions)
			w := httptest.NewRecorder()

			engine.ServeHTTP(w, req)

			if w.Code != tt.wantHTTP {
				t.Fatalf("http status = %d, want %d; body=%s", w.Code, tt.wantHTTP, w.Body.String())
			}
			if tt.wantHTTP == http.StatusOK {
				assertAPIResponseCode(t, w, tt.wantCode)
			}
		})
	}
}

func TestCAASMIngestKeyConfiguredAllowsLifecycleHooks(t *testing.T) {
	gin.SetMode(gin.TestMode)
	t.Setenv("CAASM_INGEST_API_KEY", "test-key")

	engine := gin.New()
	RegisterRoutes(engine, Dependencies{
		Auth:              authhandler.NewHandler(nil, nil, nil, nil),
		Health:            healthhandler.NewHandler(),
		CAASM:             newTestCAASMHandler(nil),
		AuthMiddleware:    allowAuth,
		RequirePermission: requireTestPermission,
	})

	req := httptest.NewRequest(http.MethodPost, "/api/v1/caasm/sync-jobs/1/fail", nil)
	req.Header.Set("X-CAASM-Ingest-Key", "test-key")
	w := httptest.NewRecorder()

	engine.ServeHTTP(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("http status = %d, want %d; body=%s", w.Code, http.StatusOK, w.Body.String())
	}
	assertAPIResponseCode(t, w, http.StatusBadRequest)
}

func TestCAASMIngestKeyRejectsMissingOrWrongHeader(t *testing.T) {
	gin.SetMode(gin.TestMode)
	t.Setenv("CAASM_INGEST_API_KEY", "test-key")

	engine := gin.New()
	RegisterRoutes(engine, Dependencies{
		Auth:              authhandler.NewHandler(nil, nil, nil, nil),
		Health:            healthhandler.NewHandler(),
		CAASM:             newTestCAASMHandler(nil),
		AuthMiddleware:    allowAuth,
		RequirePermission: requireTestPermission,
	})

	tests := []struct {
		name   string
		path   string
		header string
	}{
		{name: "ingest missing key", path: "/api/v1/caasm/ingest"},
		{name: "start wrong key", path: "/api/v1/caasm/sync-jobs/1/start", header: "wrong-key"},
		{name: "event missing key", path: "/api/v1/caasm/sync-jobs/1/events"},
		{name: "fail wrong key", path: "/api/v1/caasm/sync-jobs/1/fail", header: "wrong-key"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			req := httptest.NewRequest(http.MethodPost, tt.path, nil)
			if tt.header != "" {
				req.Header.Set("X-CAASM-Ingest-Key", tt.header)
			}
			w := httptest.NewRecorder()

			engine.ServeHTTP(w, req)

			if w.Code != http.StatusOK {
				t.Fatalf("http status = %d, want %d; body=%s", w.Code, http.StatusOK, w.Body.String())
			}
			assertAPIResponseCode(t, w, http.StatusUnauthorized)
		})
	}
}

func allowAuth(c *gin.Context) {
	permissions := splitTestPermissions(c.GetHeader("X-Test-Permissions"))
	roles := []string{}
	for _, permission := range permissions {
		if permission == "admin" {
			roles = append(roles, "admin")
			break
		}
	}
	c.Set("current_user", &jwtpkg.Claims{
		UserID:      1,
		Email:       "caasm-test@example.com",
		Roles:       roles,
		Permissions: permissions,
	})
	c.Next()
}

func requireTestPermission(permission string) gin.HandlerFunc {
	return func(c *gin.Context) {
		for _, candidate := range splitTestPermissions(c.GetHeader("X-Test-Permissions")) {
			if candidate == permission || candidate == "admin" {
				c.Next()
				return
			}
		}
		claimsValue, ok := c.Get("current_user")
		if ok {
			if claims, ok := claimsValue.(*jwtpkg.Claims); ok && claims != nil {
				for _, candidate := range claims.Permissions {
					if candidate == permission {
						c.Next()
						return
					}
				}
			}
		}
		c.AbortWithStatus(http.StatusForbidden)
	}
}

func splitTestPermissions(raw string) []string {
	if raw == "" {
		return nil
	}
	parts := strings.Split(raw, ",")
	permissions := make([]string, 0, len(parts))
	for _, part := range parts {
		permission := strings.TrimSpace(part)
		if permission != "" {
			permissions = append(permissions, permission)
		}
	}
	return permissions
}

func newTestCAASMHandler(webhooks *caasmhandler.WebhookHandler) *caasmhandler.Handler {
	assetRepo := fakeAssetRepository{}
	accountRepo := fakeCloudAccountRepository{}
	return caasmhandler.New(
		caasmservice.NewAssetService(assetRepo, nil),
		caasmservice.NewPortService(fakePortRepository{}),
		caasmservice.NewDomainService(fakeDomainRepository{}, assetRepo, nil),
		nil,
		caasmservice.NewCloudAccountService(accountRepo, fakeSyncJobRepository{}, assetRepo),
		caasmservice.NewSyncJobService(fakeSyncJobRepository{}, fakeSyncJobEventRepository{}),
		webhooks,
		nil,
		nil,
		nil,
		nil,
		nil,
	)
}

type fakeCloudAccountRepository struct{}

func (fakeCloudAccountRepository) Create(context.Context, *caasmmodel.CloudAccount) error { return nil }

func (fakeCloudAccountRepository) Update(context.Context, *caasmmodel.CloudAccount) error { return nil }

func (fakeCloudAccountRepository) Delete(context.Context, uint64) error { return nil }

func (fakeCloudAccountRepository) FindByID(context.Context, uint64) (*caasmmodel.CloudAccount, error) {
	return &caasmmodel.CloudAccount{Base: coremodel.Base{ID: 1}, Name: "aws-test", Provider: caasmmodel.CloudProviderAWS, Status: caasmmodel.CloudAccountStatusActive}, nil
}

func (fakeCloudAccountRepository) FindByExternalID(context.Context, caasmmodel.CloudProvider, string) (*caasmmodel.CloudAccount, error) {
	return nil, nil
}

func (fakeCloudAccountRepository) List(context.Context, repository.CloudAccountFilter, int, int) ([]caasmmodel.CloudAccount, int64, error) {
	return []caasmmodel.CloudAccount{{Base: coremodel.Base{ID: 1}, Name: "aws-test", Provider: caasmmodel.CloudProviderAWS, Status: caasmmodel.CloudAccountStatusActive}}, 1, nil
}

func (fakeCloudAccountRepository) FindDueForSync(context.Context) ([]caasmmodel.CloudAccount, error) {
	return nil, nil
}

type fakeAssetRepository struct{}

func (fakeAssetRepository) List(context.Context, repository.AssetFilter, int, int) ([]caasmmodel.Asset, int64, error) {
	return []caasmmodel.Asset{{Base: coremodel.Base{ID: 1}, Name: "asset-1", AssetType: caasmmodel.AssetTypeCompute, Status: caasmmodel.AssetStatusActive}}, 1, nil
}

func (fakeAssetRepository) ListIPAssets(context.Context, repository.IPAssetFilter, int, int) ([]caasmmodel.IPAsset, int64, error) {
	return []caasmmodel.IPAsset{{Base: coremodel.Base{ID: 1}, AssetID: 1, IPAddress: "203.0.113.10", IsPublic: true}}, 1, nil
}

func (fakeAssetRepository) FindByID(context.Context, uint64) (*caasmmodel.Asset, error) {
	return &caasmmodel.Asset{Base: coremodel.Base{ID: 1}, Name: "asset-1", AssetType: caasmmodel.AssetTypeCompute, Status: caasmmodel.AssetStatusActive}, nil
}

func (fakeAssetRepository) FindByNativeID(context.Context, uint64, string) (*caasmmodel.Asset, error) {
	return nil, nil
}

func (fakeAssetRepository) FindMinimalByNativeIDs(context.Context, uint64, []string) ([]repository.AssetMinimal, error) {
	return nil, nil
}

func (fakeAssetRepository) Detail(context.Context, uint64) (*repository.AssetDetail, error) {
	return &repository.AssetDetail{
		Asset: caasmmodel.Asset{
			Base:      coremodel.Base{ID: 1},
			Name:      "asset-1",
			AssetType: caasmmodel.AssetTypeCompute,
			Status:    caasmmodel.AssetStatusActive,
		},
	}, nil
}

func (fakeAssetRepository) Create(context.Context, *caasmmodel.Asset) error { return nil }

func (fakeAssetRepository) Update(context.Context, *caasmmodel.Asset) error { return nil }

func (fakeAssetRepository) UpdateAssetOwner(context.Context, uint64, *uint64, *string) error {
	return nil
}

func (fakeAssetRepository) UpdateDomainOwner(context.Context, uint64, *uint64, *string) error {
	return nil
}

func (fakeAssetRepository) Delete(context.Context, uint64) error { return nil }

func (fakeAssetRepository) Save(context.Context, *caasmmodel.Asset) error { return nil }

func (fakeAssetRepository) SaveAssetSnapshot(context.Context, *caasmmodel.Asset) error {
	return nil
}

func (fakeAssetRepository) SaveIPAsset(context.Context, *caasmmodel.IPAsset) error {
	return nil
}

func (fakeAssetRepository) SaveDomainAsset(context.Context, *caasmmodel.DomainAsset) error {
	return nil
}

func (fakeAssetRepository) SaveDomainAssetSnapshot(context.Context, *caasmmodel.DomainAsset) error {
	return nil
}

func (fakeAssetRepository) DeletePrivateIPAssetsForCloudAccount(context.Context, uint64) error {
	return nil
}

func (fakeAssetRepository) DeletePrivateDomainAssetsForCloudAccount(context.Context, uint64) error {
	return nil
}

func (fakeAssetRepository) FindIPAsset(context.Context, uint64, string) (*caasmmodel.IPAsset, error) {
	return nil, nil
}

func (fakeAssetRepository) FindDomainAsset(context.Context, uint64, string) (*caasmmodel.DomainAsset, error) {
	return nil, nil
}

func (fakeAssetRepository) ListByIDs(context.Context, []uint64) ([]caasmmodel.Asset, error) {
	return nil, nil
}

func (fakeAssetRepository) ListRelationships(context.Context, repository.RelationshipFilter) ([]caasmmodel.AssetRelationship, error) {
	return nil, nil
}

func (fakeAssetRepository) SoftDeleteStale(context.Context, uint64, time.Time) error { return nil }

func (fakeAssetRepository) GetDistinctServices(context.Context) ([]string, error) {
	return []string{"EC2"}, nil
}

func (fakeAssetRepository) GetDistinctEnvironments(context.Context) ([]string, error) {
	return []string{"prod"}, nil
}

type fakeDomainRepository struct{}

func (fakeDomainRepository) List(context.Context, repository.DomainFilter, int, int) ([]caasmmodel.DomainAsset, int64, error) {
	return []caasmmodel.DomainAsset{{Base: coremodel.Base{ID: 1}, AssetID: 1, DomainName: "example.com", Status: string(caasmmodel.AssetStatusActive)}}, 1, nil
}

func (fakeDomainRepository) Stats(context.Context, time.Duration) (*repository.DomainStats, error) {
	return &repository.DomainStats{}, nil
}

func (fakeDomainRepository) FindByID(context.Context, uint64) (*caasmmodel.DomainAsset, error) {
	return &caasmmodel.DomainAsset{Base: coremodel.Base{ID: 1}, AssetID: 1, DomainName: "example.com"}, nil
}

func (fakeDomainRepository) GetRegistrars(context.Context) ([]string, error) {
	return []string{"route53"}, nil
}

type fakePortRepository struct{}

func (fakePortRepository) Upsert(context.Context, *caasmmodel.IPPortIndex) error { return nil }

func (fakePortRepository) DeleteNonPublicForCloudAccount(context.Context, uint64) error {
	return nil
}

func (fakePortRepository) Query(context.Context, repository.PortQueryFilter, int, int) ([]caasmmodel.IPPortIndex, int64, error) {
	return []caasmmodel.IPPortIndex{{Base: coremodel.Base{ID: 1}, AssetID: 1, IPAddress: "203.0.113.10", Port: 443, Protocol: "tcp", Status: "open"}}, 1, nil
}

func (fakePortRepository) Detail(context.Context, uint64) (*repository.PortDetail, error) {
	return &repository.PortDetail{
		Port: caasmmodel.IPPortIndex{Base: coremodel.Base{ID: 1}, AssetID: 1, IPAddress: "203.0.113.10", Port: 443, Protocol: "tcp", Status: "open"},
	}, nil
}

func (fakePortRepository) Export(context.Context) ([]repository.PortExportEntry, error) {
	return []repository.PortExportEntry{{IPAddress: "203.0.113.10", Port: 443}}, nil
}

func (fakePortRepository) QueryByPort(context.Context, uint32) ([]caasmmodel.IPPortIndex, error) {
	return nil, nil
}

func (fakePortRepository) HighRiskPorts(context.Context, []uint32) ([]caasmmodel.IPPortIndex, error) {
	return nil, nil
}

func (fakePortRepository) Stats(context.Context, int) (*repository.PortStats, error) {
	return &repository.PortStats{ByStatus: map[string]int64{}}, nil
}

type fakeSyncJobEventRepository struct{}

func (fakeSyncJobEventRepository) Create(context.Context, *caasmmodel.SyncJobEvent) error {
	return nil
}

func (fakeSyncJobEventRepository) List(context.Context, uint64, repository.SyncJobEventListOptions) ([]caasmmodel.SyncJobEvent, error) {
	return []caasmmodel.SyncJobEvent{{Base: coremodel.Base{ID: 1}, SyncJobID: 1, Type: caasmmodel.EventTypeInfo, Message: "started"}}, nil
}

func assertAPIResponseCode(t *testing.T, w *httptest.ResponseRecorder, want int) {
	t.Helper()

	var body struct {
		Code int `json:"code"`
	}
	if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
		t.Fatalf("decode API response: %v; body=%s", err, w.Body.String())
	}
	if body.Code != want {
		t.Fatalf("api code = %d, want %d; body=%s", body.Code, want, w.Body.String())
	}
}

type fakeSyncJobRepository struct{}

func (fakeSyncJobRepository) Create(context.Context, *caasmmodel.SyncJob) error { return nil }

func (fakeSyncJobRepository) Update(context.Context, *caasmmodel.SyncJob) error { return nil }

func (fakeSyncJobRepository) UpdateFields(context.Context, uint64, map[string]interface{}) error {
	return nil
}

func (fakeSyncJobRepository) FindByID(context.Context, uint64) (*caasmmodel.SyncJob, error) {
	return &caasmmodel.SyncJob{Status: caasmmodel.SyncJobRunning}, nil
}

func (fakeSyncJobRepository) List(context.Context, repository.SyncJobFilter, int, int) ([]caasmmodel.SyncJob, int64, error) {
	return nil, 0, nil
}

func (fakeSyncJobRepository) FindStaleJobs(context.Context, caasmmodel.SyncJobStatus, time.Time) ([]caasmmodel.SyncJob, error) {
	return nil, nil
}

func (fakeSyncJobRepository) CountActiveJobs(context.Context) (int64, error) { return 0, nil }

func (fakeSyncJobRepository) CountRunningJobs(context.Context) (int64, error) { return 0, nil }

func (fakeSyncJobRepository) HasActiveJobForAccount(context.Context, uint64) (bool, error) {
	return false, nil
}

func (fakeSyncJobRepository) FindActiveJobForAccount(context.Context, uint64) (*caasmmodel.SyncJob, error) {
	return nil, nil
}

func (fakeSyncJobRepository) FindLatestResumableJobForAccount(context.Context, uint64, uint64, string) (*caasmmodel.SyncJob, error) {
	return nil, nil
}
