package webhook_test

import (
	"bytes"
	"encoding/json"
	"io"
	"log/slog"
	"net/http"
	"net/http/httptest"
	"testing"

	apicontract "bit.admedia.com/scm/ad/adpilot-indexing-repo-sync.com/internal/contracts/api"
	webhookcontract "bit.admedia.com/scm/ad/adpilot-indexing-repo-sync.com/internal/contracts/webhook"
	"bit.admedia.com/scm/ad/adpilot-indexing-repo-sync.com/internal/webhook"
)

type mockSyncTrigger struct {
	calls int
	jobID string
	err   error
}

func (m *mockSyncTrigger) TriggerSync(repoID, ref string) (string, error) {
	m.calls++
	if m.err != nil {
		return "", m.err
	}
	if m.jobID != "" {
		return m.jobID, nil
	}
	return "job_webhook123456", nil
}

func testLogger() *slog.Logger {
	return slog.New(slog.NewTextHandler(io.Discard, nil))
}

func signedRequest(t *testing.T, secret string, body []byte, eventKey string) *http.Request {
	t.Helper()
	req := httptest.NewRequest(http.MethodPost, "/webhooks/bitbucket", bytes.NewReader(body))
	req.Header.Set("X-Event-Key", eventKey)
	req.Header.Set("X-Hub-Signature", webhook.SignBody(secret, body))
	req.Header.Set("X-Request-Id", "req-test-1")
	return req
}

func TestHandlerAccepted(t *testing.T) {
	trigger := &mockSyncTrigger{jobID: "job_accepted"}
	h := webhook.NewHandler("secret", "staging", trigger, testLogger())

	body := loadFixture(t, "refs_changed_master.json")
	req := signedRequest(t, "secret", body, webhookcontract.EventRefsChanged)
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)

	if rec.Code != http.StatusAccepted {
		t.Fatalf("status: got %d body=%s", rec.Code, rec.Body.String())
	}
	if trigger.calls != 1 {
		t.Fatalf("trigger calls: got %d", trigger.calls)
	}

	var resp apicontract.WebhookResponse
	if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
		t.Fatalf("decode: %v", err)
	}
	if resp.Status != "accepted" || resp.JobID != "job_accepted" || resp.Ref != "master" {
		t.Fatalf("unexpected response: %+v", resp)
	}
}

func TestHandlerInvalidSignature(t *testing.T) {
	h := webhook.NewHandler("secret", "staging", &mockSyncTrigger{}, testLogger())
	body := loadFixture(t, "refs_changed_master.json")
	req := httptest.NewRequest(http.MethodPost, "/webhooks/bitbucket", bytes.NewReader(body))
	req.Header.Set("X-Event-Key", webhookcontract.EventRefsChanged)
	req.Header.Set("X-Hub-Signature", "sha256=deadbeef")

	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)

	if rec.Code != http.StatusUnauthorized {
		t.Fatalf("status: got %d", rec.Code)
	}
}

func TestHandlerBadJSON(t *testing.T) {
	h := webhook.NewHandler("secret", "staging", &mockSyncTrigger{}, testLogger())
	body := []byte("{")
	req := signedRequest(t, "secret", body, webhookcontract.EventRefsChanged)

	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)

	if rec.Code != http.StatusBadRequest {
		t.Fatalf("status: got %d", rec.Code)
	}
}

func TestHandlerIgnoredNonMaster(t *testing.T) {
	trigger := &mockSyncTrigger{}
	h := webhook.NewHandler("secret", "staging", trigger, testLogger())

	body := loadFixture(t, "refs_changed_feature_branch.json")
	req := signedRequest(t, "secret", body, webhookcontract.EventRefsChanged)
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)

	if rec.Code != http.StatusAccepted {
		t.Fatalf("status: got %d", rec.Code)
	}
	if trigger.calls != 0 {
		t.Fatal("expected sync not to be triggered")
	}

	var resp apicontract.WebhookResponse
	if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
		t.Fatalf("decode: %v", err)
	}
	if resp.Status != "ignored" || resp.Reason != "not master branch" {
		t.Fatalf("unexpected response: %+v", resp)
	}
}

func TestHandlerIgnoredUnsupportedEvent(t *testing.T) {
	trigger := &mockSyncTrigger{}
	h := webhook.NewHandler("secret", "staging", trigger, testLogger())

	body := loadFixture(t, "refs_changed_master.json")
	req := signedRequest(t, "secret", body, "repo:modified")
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)

	if rec.Code != http.StatusAccepted {
		t.Fatalf("status: got %d", rec.Code)
	}
	if trigger.calls != 0 {
		t.Fatal("expected sync not to be triggered")
	}

	var resp apicontract.WebhookResponse
	if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
		t.Fatalf("decode: %v", err)
	}
	if resp.Status != "ignored" || resp.Reason != "unsupported event" {
		t.Fatalf("unexpected response: %+v", resp)
	}
}

func TestHandlerDuplicateCommit(t *testing.T) {
	trigger := &mockSyncTrigger{}
	h := webhook.NewHandler("secret", "staging", trigger, testLogger())
	body := loadFixture(t, "refs_changed_master.json")

	req1 := signedRequest(t, "secret", body, webhookcontract.EventRefsChanged)
	rec1 := httptest.NewRecorder()
	h.ServeHTTP(rec1, req1)
	if rec1.Code != http.StatusAccepted {
		t.Fatalf("first status: got %d", rec1.Code)
	}

	req2 := signedRequest(t, "secret", body, webhookcontract.EventRefsChanged)
	rec2 := httptest.NewRecorder()
	h.ServeHTTP(rec2, req2)
	if rec2.Code != http.StatusAccepted {
		t.Fatalf("second status: got %d", rec2.Code)
	}

	var resp apicontract.WebhookResponse
	if err := json.Unmarshal(rec2.Body.Bytes(), &resp); err != nil {
		t.Fatalf("decode: %v", err)
	}
	if resp.Status != "ignored" || resp.Reason != "duplicate commit" {
		t.Fatalf("unexpected response: %+v", resp)
	}
	if trigger.calls != 1 {
		t.Fatalf("trigger calls: got %d, want 1", trigger.calls)
	}
}

func TestHandlerLocalSkipsSignatureWhenSecretEmpty(t *testing.T) {
	trigger := &mockSyncTrigger{}
	h := webhook.NewHandler("", "local", trigger, testLogger())
	body := loadFixture(t, "refs_changed_master.json")

	req := httptest.NewRequest(http.MethodPost, "/webhooks/bitbucket", bytes.NewReader(body))
	req.Header.Set("X-Event-Key", webhookcontract.EventRefsChanged)

	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)

	if rec.Code != http.StatusAccepted {
		t.Fatalf("status: got %d body=%s", rec.Code, rec.Body.String())
	}
	if trigger.calls != 1 {
		t.Fatalf("trigger calls: got %d", trigger.calls)
	}
}
