package handler

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

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

	scannerclient "security-center-backend/internal/modules/scan/client"
	"security-center-backend/internal/modules/scan/service"
)

func TestCallbackRejectsInvalidBearerTokenWithReal401(t *testing.T) {
	router := callbackRouter(newFakeRepo())
	req := newCallbackRequest(`{"batch_id":"scanner-1","request_id":"client-1","status":"completed","observations":[]}`, "wrong-token", "idem-1")
	resp := httptest.NewRecorder()

	router.ServeHTTP(resp, req)

	require.Equal(t, http.StatusUnauthorized, resp.Code)
	require.JSONEq(t, `{"error":"invalid callback token"}`, resp.Body.String())
}

func TestCallbackRejectsPayloadHashMismatchWithReal400(t *testing.T) {
	router := callbackRouter(newFakeRepo())
	req := newCallbackRequest(`{"batch_id":"scanner-1","request_id":"client-1","status":"completed","observations":[]}`, "callback-token", "idem-1")
	req.Header.Set("X-Payload-SHA256", strings.Repeat("0", 64))
	resp := httptest.NewRecorder()

	router.ServeHTTP(resp, req)

	require.Equal(t, http.StatusBadRequest, resp.Code)
	require.JSONEq(t, `{"error":"payload hash mismatch"}`, resp.Body.String())
}

func TestCallbackDuplicateSameHashReturnsReal200(t *testing.T) {
	repo := newFakeRepo()
	body := `{"batch_id":"scanner-1","request_id":"client-1","status":"completed","observations":[]}`
	hash := payloadHash(body)
	repo.receipts["idem-1"] = &service.CallbackReceipt{
		ID:             10,
		IdempotencyKey: "idem-1",
		LocalBatchID:   ptrUint64(1),
		PayloadSHA256:  hash,
	}
	router := callbackRouter(repo)
	req := newCallbackRequest(body, "callback-token", "idem-1")
	resp := httptest.NewRecorder()

	router.ServeHTTP(resp, req)

	require.Equal(t, http.StatusOK, resp.Code)
	require.JSONEq(t, `{"status":"duplicate"}`, resp.Body.String())
}

func TestCallbackSameKeyDifferentHashReturnsReal409(t *testing.T) {
	repo := newFakeRepo()
	repo.receipts["idem-1"] = &service.CallbackReceipt{
		ID:             10,
		IdempotencyKey: "idem-1",
		LocalBatchID:   ptrUint64(1),
		PayloadSHA256:  strings.Repeat("a", 64),
	}
	router := callbackRouter(repo)
	req := newCallbackRequest(`{"batch_id":"scanner-1","request_id":"client-1","status":"completed","observations":[]}`, "callback-token", "idem-1")
	resp := httptest.NewRecorder()

	router.ServeHTTP(resp, req)

	require.Equal(t, http.StatusConflict, resp.Code)
	require.JSONEq(t, `{"error":"idempotency key payload conflict"}`, resp.Body.String())
}

func TestCallbackAcceptedAppliesEndpointFallbackIdentity(t *testing.T) {
	repo := newFakeRepo()
	router := callbackRouter(repo)
	body := `{"batch_id":"scanner-1","request_id":"client-1","status":"completed","observations":[{"public_ip":"8.8.8.8","port":443,"transport":"tcp","result":"confirmed_open","reason_code":"tcp_connect"}]}`
	req := newCallbackRequest(body, "callback-token", "idem-1")
	resp := httptest.NewRecorder()

	router.ServeHTTP(resp, req)

	require.Equal(t, http.StatusOK, resp.Code)
	require.JSONEq(t, `{"status":"accepted"}`, resp.Body.String())
	require.Len(t, repo.applied.Observations, 1)
	require.Equal(t, service.ObservationIdentityEndpoint, repo.applied.Observations[0].IdentityKind)
	require.Equal(t, "8.8.8.8|443|tcp", repo.applied.Observations[0].IdentityValue)
}

func TestCallbackAcceptedAggregatesParentCampaign(t *testing.T) {
	repo := newFakeRepo()
	campaignID := uint64(10)
	repo.batch.CampaignID = &campaignID
	router := callbackRouter(repo)
	body := `{"batch_id":"scanner-1","request_id":"client-1","status":"completed","observations":[{"public_ip":"8.8.8.8","port":443,"transport":"tcp","result":"confirmed_open","reason_code":"tcp_connect"}]}`
	req := newCallbackRequest(body, "callback-token", "idem-1")
	resp := httptest.NewRecorder()

	router.ServeHTTP(resp, req)

	require.Equal(t, http.StatusOK, resp.Code)
	require.Equal(t, campaignID, repo.aggregatedCampaignID)
}

func TestManageRoutesCancelBatchUsesServiceCancel(t *testing.T) {
	gin.SetMode(gin.TestMode)
	repo := newFakeRepo()
	scanner := &fakeScanner{}
	scanService := service.New(repo, nil, scanner, service.Config{})
	h := New(scanService)
	router := gin.New()
	h.RegisterManageRoutes(router.Group("/api/v1"))
	req := httptest.NewRequest(http.MethodPost, "/api/v1/scans/batches/1/cancel", nil)
	resp := httptest.NewRecorder()

	router.ServeHTTP(resp, req)

	require.Equal(t, http.StatusOK, resp.Code)
	require.Equal(t, []string{"scanner-1"}, scanner.cancelIDs)
	require.Equal(t, "canceled", repo.batch.Status)
}

func callbackRouter(repo *fakeRepo) *gin.Engine {
	gin.SetMode(gin.TestMode)
	scanService := service.New(repo, nil, nil, service.Config{CallbackToken: "callback-token"})
	h := New(scanService)
	router := gin.New()
	h.RegisterCallbackRoutes(router.Group("/api/v1"))
	return router
}

func newCallbackRequest(body string, token string, idempotencyKey string) *http.Request {
	req := httptest.NewRequest(http.MethodPost, "/api/v1/scans/callback", strings.NewReader(body))
	req.Header.Set("Authorization", "Bearer "+token)
	req.Header.Set("Idempotency-Key", idempotencyKey)
	req.Header.Set("X-Request-ID", "scanner-callback-1")
	req.Header.Set("X-Payload-SHA256", payloadHash(body))
	return req
}

func payloadHash(body string) string {
	sum := sha256.Sum256([]byte(body))
	return hex.EncodeToString(sum[:])
}

func ptrUint64(v uint64) *uint64 {
	return &v
}

type fakeRepo struct {
	batch                *service.Batch
	receipts             map[string]*service.CallbackReceipt
	applied              service.ApplyObservationsInput
	aggregatedCampaignID uint64
}

type fakeScanner struct {
	cancelIDs []string
}

func (f *fakeScanner) CreateBatch(context.Context, scannerclient.CreateBatchRequest) (*scannerclient.CreateBatchResponse, error) {
	return nil, nil
}

func (f *fakeScanner) GetBatch(context.Context, string) (*scannerclient.BatchResponse, error) {
	return nil, nil
}

func (f *fakeScanner) GetResults(context.Context, string) (*scannerclient.ResultsResponse, error) {
	return nil, nil
}

func (f *fakeScanner) CancelBatch(_ context.Context, scannerBatchID string, _ string) (*scannerclient.BatchResponse, error) {
	f.cancelIDs = append(f.cancelIDs, scannerBatchID)
	return &scannerclient.BatchResponse{BatchID: scannerBatchID, Status: "canceled"}, nil
}

func newFakeRepo() *fakeRepo {
	return &fakeRepo{
		batch: &service.Batch{
			ID:             1,
			RequestID:      "client-1",
			ScannerBatchID: "scanner-1",
			Status:         "running",
		},
		receipts: make(map[string]*service.CallbackReceipt),
	}
}

func (f *fakeRepo) CreateBatch(context.Context, service.CreateBatchRecordInput) (*service.Batch, error) {
	return nil, nil
}

func (f *fakeRepo) CreateCampaign(context.Context, service.CreateCampaignRecordInput) (*service.Campaign, error) {
	return nil, nil
}

func (f *fakeRepo) GetCampaign(context.Context, uint64) (*service.Campaign, error) {
	return nil, nil
}

func (f *fakeRepo) FindCampaignByRequestID(context.Context, string) (*service.Campaign, error) {
	return nil, nil
}

func (f *fakeRepo) ListCampaigns(context.Context, service.ListCampaignsFilter) (*service.PaginatedResult[service.Campaign], error) {
	return nil, nil
}

func (f *fakeRepo) ListCampaignBatches(context.Context, uint64, service.ListBatchesFilter) (*service.PaginatedResult[service.Batch], error) {
	return nil, nil
}

func (f *fakeRepo) ClaimNextCampaign(context.Context) (*service.Campaign, error) {
	return nil, nil
}

func (f *fakeRepo) UpdateCampaignFields(context.Context, uint64, service.CampaignUpdateFields) error {
	return nil
}

func (f *fakeRepo) UpdateCampaignBatchPartCount(context.Context, uint64, int) error {
	return nil
}

func (f *fakeRepo) AggregateCampaign(_ context.Context, campaignID uint64) (*service.Campaign, error) {
	f.aggregatedCampaignID = campaignID
	return &service.Campaign{ID: campaignID}, nil
}

func (f *fakeRepo) MarkBatchSubmitted(context.Context, uint64, service.BatchSubmittedInput) (*service.Batch, error) {
	return nil, nil
}

func (f *fakeRepo) MarkBatchSubmitFailed(context.Context, uint64, string) error {
	return nil
}

func (f *fakeRepo) GetBatch(context.Context, uint64) (*service.Batch, error) {
	return f.batch, nil
}

func (f *fakeRepo) FindBatchForCallback(_ context.Context, requestID string, scannerBatchID string) (*service.Batch, error) {
	if requestID == f.batch.RequestID || scannerBatchID == f.batch.ScannerBatchID {
		return f.batch, nil
	}
	return nil, nil
}

func (f *fakeRepo) ListBatches(context.Context, service.ListBatchesFilter) (*service.PaginatedResult[service.Batch], error) {
	return nil, nil
}

func (f *fakeRepo) ListTargets(context.Context, uint64, service.ListTargetsFilter) (*service.PaginatedResult[service.Target], error) {
	return nil, nil
}

func (f *fakeRepo) ListObservations(context.Context, uint64, service.Pagination) (*service.PaginatedResult[service.Observation], error) {
	return nil, nil
}

func (f *fakeRepo) ListCallbackReceipts(context.Context, uint64, service.Pagination) (*service.PaginatedResult[service.CallbackReceipt], error) {
	return nil, nil
}

func (f *fakeRepo) ListPortExposures(context.Context, service.ListPortExposuresFilter) (*service.PaginatedResult[service.PortExposure], error) {
	return nil, nil
}

func (f *fakeRepo) GetStats(context.Context) (*service.Stats, error) {
	return nil, nil
}

func (f *fakeRepo) FindCallbackReceipt(_ context.Context, idempotencyKey string) (*service.CallbackReceipt, error) {
	return f.receipts[idempotencyKey], nil
}

func (f *fakeRepo) CreateCallbackReceipt(_ context.Context, input service.CallbackReceiptInput) (*service.CallbackReceipt, error) {
	receipt := &service.CallbackReceipt{
		ID:             uint64(len(f.receipts) + 1),
		IdempotencyKey: input.IdempotencyKey,
		LocalBatchID:   input.LocalBatchID,
		PayloadSHA256:  input.PayloadSHA256,
	}
	f.receipts[input.IdempotencyKey] = receipt
	return receipt, nil
}

func (f *fakeRepo) MarkCallbackReceiptApplied(context.Context, uint64, uint64) error {
	return nil
}

func (f *fakeRepo) MarkCallbackReceiptFailed(context.Context, uint64, uint64, string, int, string) error {
	return nil
}

func (f *fakeRepo) ApplyObservations(_ context.Context, input service.ApplyObservationsInput) error {
	f.applied = input
	return nil
}

func (f *fakeRepo) UpdateBatchFromSync(_ context.Context, _ uint64, batch *scannerclient.BatchResponse, _ *scannerclient.ResultsResponse) error {
	if batch != nil && batch.Status != "" {
		f.batch.Status = batch.Status
	}
	return nil
}
