package http

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

type fakeGitoBranchEventHandler struct {
	callCount int
	err       error
	events    []GitoBranchUpdatedEvent
}

func (f *fakeGitoBranchEventHandler) Handle(_ context.Context, ev GitoBranchUpdatedEvent) error {
	f.callCount++
	f.events = append(f.events, ev)
	return f.err
}

func testGitoSignature(secret, body string) string {
	mac := hmac.New(sha256.New, []byte(secret))
	_, _ = mac.Write([]byte(body))
	return hex.EncodeToString(mac.Sum(nil))
}

func newGitoHandlerForTest() *Handler {
	h := newHandlerForTest()
	h.SetGitoWebhookConfig(GitoWebhookConfig{
		Secret: "secret",
		RepoID: "target-repo",
		Branch: "develop",
	})
	return h
}

const gitoTargetBody = `{"repo_id":"target-repo","branch":"develop","after":"abc123"}`

func TestReceiveGitoWebhookAcceptsSignedBranchUpdatedDelivery(t *testing.T) {
	fake := &fakeGitoBranchEventHandler{}
	h := newGitoHandlerForTest()
	h.SetGitoBranchEventHandler(fake)

	req := httptest.NewRequest(http.MethodPost, "/api/integrations/gito/webhook", strings.NewReader(gitoTargetBody))
	req.Header.Set("X-Gito-Signature", testGitoSignature("secret", gitoTargetBody))
	req.Header.Set("X-Gito-Event", "branch.updated")
	req.Header.Set("X-Gito-Delivery", "delivery-1")
	rec := httptest.NewRecorder()

	h.ReceiveGitoWebhook(rec, req)

	if rec.Code != http.StatusAccepted {
		t.Fatalf("expected 202, got %d: %s", rec.Code, rec.Body.String())
	}
	if fake.callCount != 1 {
		t.Fatalf("expected handler called once, got %d", fake.callCount)
	}
}

func TestReceiveGitoWebhookRejectsWrongEventWithoutHandlerCall(t *testing.T) {
	fake := &fakeGitoBranchEventHandler{}
	h := newGitoHandlerForTest()
	h.SetGitoBranchEventHandler(fake)

	req := httptest.NewRequest(http.MethodPost, "/api/integrations/gito/webhook", strings.NewReader(gitoTargetBody))
	req.Header.Set("X-Gito-Signature", testGitoSignature("secret", gitoTargetBody))
	req.Header.Set("X-Gito-Event", "push")
	rec := httptest.NewRecorder()

	h.ReceiveGitoWebhook(rec, req)

	if rec.Code != http.StatusBadRequest {
		t.Fatalf("expected 400, got %d: %s", rec.Code, rec.Body.String())
	}
	if fake.callCount != 0 {
		t.Fatalf("expected handler not called, got %d", fake.callCount)
	}
}

func TestReceiveGitoWebhookRejectsInvalidSignatureWithoutHandlerCall(t *testing.T) {
	fake := &fakeGitoBranchEventHandler{}
	h := newGitoHandlerForTest()
	h.SetGitoBranchEventHandler(fake)

	req := httptest.NewRequest(http.MethodPost, "/api/integrations/gito/webhook", strings.NewReader(gitoTargetBody))
	req.Header.Set("X-Gito-Signature", testGitoSignature("wrong-secret", gitoTargetBody))
	req.Header.Set("X-Gito-Event", "branch.updated")
	rec := httptest.NewRecorder()

	h.ReceiveGitoWebhook(rec, req)

	if rec.Code != http.StatusUnauthorized {
		t.Fatalf("expected 401, got %d: %s", rec.Code, rec.Body.String())
	}
	if fake.callCount != 0 {
		t.Fatalf("expected handler not called, got %d", fake.callCount)
	}
}

func TestGitoWebhookRouteBypassesBasicAuth(t *testing.T) {
	h := newHandlerForTest()
	h.SetGitoWebhookConfig(GitoWebhookConfig{Secret: "secret", RepoID: "target-repo", Branch: "develop"})

	// Off-target repo so the event is ignored without side effects.
	body := `{"repo_id":"other-repo","branch":"develop"}`
	req := httptest.NewRequest(http.MethodPost, "/api/integrations/gito/webhook", strings.NewReader(body))
	req.Header.Set("X-Gito-Signature", testGitoSignature("secret", body))
	req.Header.Set("X-Gito-Event", "branch.updated")
	req.Header.Set("X-Gito-Delivery", "delivery-off-target")
	rec := httptest.NewRecorder()

	router := NewRouter(h, nil, AuthConfig{Username: "nomadcode", Password: "required"}, nil, "")
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusAccepted {
		t.Fatalf("expected webhook route to bypass basic auth, got %d: %s", rec.Code, rec.Body.String())
	}
}

func TestReceiveGitoWebhookDeduplicatesSameDeliveryID(t *testing.T) {
	fake := &fakeGitoBranchEventHandler{}
	h := newGitoHandlerForTest()
	h.SetGitoBranchEventHandler(fake)

	for i := range 2 {
		req := httptest.NewRequest(http.MethodPost, "/api/integrations/gito/webhook", strings.NewReader(gitoTargetBody))
		req.Header.Set("X-Gito-Signature", testGitoSignature("secret", gitoTargetBody))
		req.Header.Set("X-Gito-Event", "branch.updated")
		req.Header.Set("X-Gito-Delivery", "delivery-1")
		rec := httptest.NewRecorder()
		h.ReceiveGitoWebhook(rec, req)
		if rec.Code != http.StatusAccepted {
			t.Fatalf("call %d: expected 202, got %d: %s", i+1, rec.Code, rec.Body.String())
		}
	}

	if fake.callCount != 1 {
		t.Fatalf("expected handler called once, got %d", fake.callCount)
	}
}

func TestReceiveGitoWebhookRejectsMissingDeliveryIDWithoutHandlerCall(t *testing.T) {
	fake := &fakeGitoBranchEventHandler{}
	h := newGitoHandlerForTest()
	h.SetGitoBranchEventHandler(fake)

	req := httptest.NewRequest(http.MethodPost, "/api/integrations/gito/webhook", strings.NewReader(gitoTargetBody))
	req.Header.Set("X-Gito-Signature", testGitoSignature("secret", gitoTargetBody))
	req.Header.Set("X-Gito-Event", "branch.updated")
	rec := httptest.NewRecorder()

	h.ReceiveGitoWebhook(rec, req)

	if rec.Code != http.StatusBadRequest {
		t.Fatalf("expected 400, got %d: %s", rec.Code, rec.Body.String())
	}
	if fake.callCount != 0 {
		t.Fatalf("expected handler not called, got %d", fake.callCount)
	}
}

func TestReceiveGitoWebhookRejectsWrongBodyTypeWithoutHandlerCall(t *testing.T) {
	fake := &fakeGitoBranchEventHandler{}
	h := newGitoHandlerForTest()
	h.SetGitoBranchEventHandler(fake)

	// body type이 "push" 인 경우 - header event 가 branch.updated여도 body type 검증 통과 못함
	wrongTypeBody := `{"repo_id":"target-repo","branch":"develop","type":"push"}`
	req := httptest.NewRequest(http.MethodPost, "/api/integrations/gito/webhook", strings.NewReader(wrongTypeBody))
	req.Header.Set("X-Gito-Signature", testGitoSignature("secret", wrongTypeBody))
	req.Header.Set("X-Gito-Event", "branch.updated")
	req.Header.Set("X-Gito-Delivery", "delivery-wrong-type")
	rec := httptest.NewRecorder()

	h.ReceiveGitoWebhook(rec, req)

	if rec.Code != http.StatusBadRequest {
		t.Fatalf("expected 400, got %d: %s", rec.Code, rec.Body.String())
	}
	if fake.callCount != 0 {
		t.Fatalf("expected handler not called, got %d", fake.callCount)
	}
}

func TestReceiveGitoWebhookRejectsMalformedChangedFilesWithoutHandlerCall(t *testing.T) {
	fake := &fakeGitoBranchEventHandler{}
	h := newGitoHandlerForTest()
	h.SetGitoBranchEventHandler(fake)

	// changed_files 가 list 가 아닌 string 인 경우
	malformedBody := `{"repo_id":"target-repo","branch":"develop","changed_files":"not-a-list"}`
	req := httptest.NewRequest(http.MethodPost, "/api/integrations/gito/webhook", strings.NewReader(malformedBody))
	req.Header.Set("X-Gito-Signature", testGitoSignature("secret", malformedBody))
	req.Header.Set("X-Gito-Event", "branch.updated")
	req.Header.Set("X-Gito-Delivery", "delivery-malformed-files")
	rec := httptest.NewRecorder()

	h.ReceiveGitoWebhook(rec, req)

	if rec.Code != http.StatusBadRequest {
		t.Fatalf("expected 400, got %d: %s", rec.Code, rec.Body.String())
	}
	if fake.callCount != 0 {
		t.Fatalf("expected handler not called, got %d", fake.callCount)
	}
}

func TestReceiveGitoWebhookRejectsMalformedChangedFileItemWithoutHandlerCall(t *testing.T) {
	fake := &fakeGitoBranchEventHandler{}
	h := newGitoHandlerForTest()
	h.SetGitoBranchEventHandler(fake)

	// changed_files 가 list 이지만 element 가 object 가 아닌 경우
	malformedItemBody := `{"repo_id":"target-repo","branch":"develop","changed_files":["not-an-object"]}`
	req := httptest.NewRequest(http.MethodPost, "/api/integrations/gito/webhook", strings.NewReader(malformedItemBody))
	req.Header.Set("X-Gito-Signature", testGitoSignature("secret", malformedItemBody))
	req.Header.Set("X-Gito-Event", "branch.updated")
	req.Header.Set("X-Gito-Delivery", "delivery-malformed-file-item")
	rec := httptest.NewRecorder()

	h.ReceiveGitoWebhook(rec, req)

	if rec.Code != http.StatusBadRequest {
		t.Fatalf("expected 400, got %d: %s", rec.Code, rec.Body.String())
	}
	if fake.callCount != 0 {
		t.Fatalf("expected handler not called, got %d", fake.callCount)
	}
}

func TestReceiveGitoWebhookHandlerErrorDoesNotMarkDuplicateKeys(t *testing.T) {
	fake := &fakeGitoBranchEventHandler{err: errors.New("revision not ready")}
	h := newGitoHandlerForTest()
	h.SetGitoBranchEventHandler(fake)

	req := httptest.NewRequest(http.MethodPost, "/api/integrations/gito/webhook", strings.NewReader(gitoTargetBody))
	req.Header.Set("X-Gito-Signature", testGitoSignature("secret", gitoTargetBody))
	req.Header.Set("X-Gito-Event", "branch.updated")
	req.Header.Set("X-Gito-Delivery", "delivery-retry")
	rec := httptest.NewRecorder()

	h.ReceiveGitoWebhook(rec, req)

	if rec.Code != http.StatusInternalServerError {
		t.Fatalf("expected 500 for handler error, got %d: %s", rec.Code, rec.Body.String())
	}
	if fake.callCount != 1 {
		t.Fatalf("expected handler called once before retry, got %d", fake.callCount)
	}

	fake.err = nil
	retry := httptest.NewRequest(http.MethodPost, "/api/integrations/gito/webhook", strings.NewReader(gitoTargetBody))
	retry.Header.Set("X-Gito-Signature", testGitoSignature("secret", gitoTargetBody))
	retry.Header.Set("X-Gito-Event", "branch.updated")
	retry.Header.Set("X-Gito-Delivery", "delivery-retry")
	retryRec := httptest.NewRecorder()

	h.ReceiveGitoWebhook(retryRec, retry)

	if retryRec.Code != http.StatusAccepted {
		t.Fatalf("expected retry to be accepted, got %d: %s", retryRec.Code, retryRec.Body.String())
	}
	if fake.callCount != 2 {
		t.Fatalf("expected retry to reach handler because failed delivery was not marked duplicate, got %d calls", fake.callCount)
	}
}
