package main

import (
	"bytes"
	"context"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/nomadcode/nomadcode-core/internal/gitoevents"
	"github.com/nomadcode/nomadcode-core/internal/gitosync"
	apphttp "github.com/nomadcode/nomadcode-core/internal/http"
	"github.com/nomadcode/nomadcode-core/internal/roadmapsync"
	"github.com/nomadcode/nomadcode-core/internal/scheduler"
	"github.com/nomadcode/nomadcode-core/internal/workitem"
)

// ---------------------------------------------------------------------------
// Test fakes for the bridge seams
// ---------------------------------------------------------------------------

// fakeScanner records the event passed to Scan and returns a scanned result.
type fakeScanner struct {
	lastEvent gitoevents.BranchUpdatedEvent
	out       gitosync.ScanOutput
	err       error
}

func (f *fakeScanner) Scan(_ context.Context, ev gitoevents.BranchUpdatedEvent) (gitosync.ScanOutput, error) {
	f.lastEvent = ev
	if f.err != nil {
		return gitosync.ScanOutput{}, f.err
	}
	return f.out, nil
}

// fakeEnqueuer records jobs passed to EnqueueRoadmapCreationSync.
type fakeEnqueuer struct {
	jobs []scheduler.RoadmapCreationSyncJobArgs
	err  error
}

func (f *fakeEnqueuer) EnqueueRoadmapCreationSync(_ context.Context, args scheduler.RoadmapCreationSyncJobArgs) error {
	if f.err != nil {
		return f.err
	}
	f.jobs = append(f.jobs, args)
	return nil
}

// fakeReader records refs passed to FetchWorkItem.
type fakeReader struct {
	item workitem.WorkItem
	err  error
	refs []workitem.Ref
}

func (f *fakeReader) FetchWorkItem(_ context.Context, ref workitem.Ref) (workitem.WorkItem, error) {
	f.refs = append(f.refs, ref)
	if f.err != nil {
		return workitem.WorkItem{}, f.err
	}
	return f.item, nil
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

// signBody creates an HMAC-SHA256 signature for the given body and secret.
func signBody(secret string, body []byte) string {
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write(body)
	return hex.EncodeToString(mac.Sum(nil))
}

// buildBranchUpdatedPayload creates a minimal branch.updated webhook body.
func buildBranchUpdatedPayload(repoID, branch, before, after string) []byte {
	payload := map[string]interface{}{
		"type":          "branch.updated",
		"repo_id":       repoID,
		"branch":        branch,
		"before":        before,
		"after":         after,
		"changed_files": []interface{}{},
	}
	data, err := json.Marshal(payload)
	if err != nil {
		panic("buildBranchUpdatedPayload: marshal: " + err.Error())
	}
	return data
}

// matchedScanOutput returns a ScanOutput with one matched milestone identity.
func matchedScanOutput() gitosync.ScanOutput {
	identity := roadmapsync.Identity{
		Shape:                roadmapsync.ShapeMilestone,
		RoadmapMilestonePath: "agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/gito-http-webhook-consumer-readiness.md",
		Provider:             "plane",
		Tenant:               "acme",
		Project:              "proj-1",
		WorkItemID:           "wi-123",
	}
	normalized, err := identity.Normalize()
	if err != nil {
		panic("matchedScanOutput: Normalize: " + err.Error())
	}
	scanned := roadmapsync.ScannedMilestone{
		Path:     "agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/gito-http-webhook-consumer-readiness.md",
		Identity: normalized,
	}
	return gitosync.ScanOutput{
		Result: roadmapsync.ScanResult{
			Revision: roadmapsync.Revision{
				Branch:   "develop",
				Revision: "abc123def456",
			},
			ChangedFiles:      []string{"agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/gito-http-webhook-consumer-readiness.md"},
			ScannedMilestones: []roadmapsync.ScannedMilestone{scanned},
		},
		Docs: []gitosync.ScannedMilestoneDoc{
			{
				Milestone: scanned,
				Markdown:  "# Milestone Title\n\nbody content",
			},
		},
	}
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

// TestGitoHTTPWebhookDeliveryEnqueuesCreationSyncOnce verifies that a signed
// HTTP branch.updated delivery reaches the bridge scanner and triggers exactly
// one EnqueueRoadmapCreationSync call.
func TestGitoHTTPWebhookDeliveryEnqueuesCreationSyncOnce(t *testing.T) {
	const (
		secret = "test-webhook-secret"
		repoID = "nomadcode"
		branch = "develop"
		before = "0000000000000000000000000000000000000000"
		after  = "abc123def456"
	)

	// 1. Set up fakes.
	scanner := &fakeScanner{
		out: matchedScanOutput(),
	}
	enq := &fakeEnqueuer{}
	reader := &fakeReader{
		item: workitem.WorkItem{
			Title:           "Gito HTTP Webhook Consumer Readiness",
			DescriptionHTML: "<p>Original HTML body</p>",
			DescriptionText: "Original text body",
			State:           "backlog",
		},
	}

	// 2. Build the bridge directly with fakes.
	bridge, err := gitosync.NewBridge(
		scanner,
		reader,
		enq,
		gitosync.NewInMemoryRevisionStore(),
		gitosync.BridgeConfig{
			RepoID:      repoID,
			Branch:      branch,
			TodoStateID: "state-todo",
		},
		nil, // logger
	)
	if err != nil {
		t.Fatalf("NewBridge: %v", err)
	}

	// 3. Create the HTTP adapter.
	adapter := &gitoBridgeHTTPAdapter{bridge: bridge}

	// 4. Set up the HTTP handler with config and event handler.
	cfg := apphttp.GitoWebhookConfig{
		Secret: secret,
		RepoID: repoID,
		Branch: branch,
	}

	handler := apphttp.NewHandler(nil, nil, nil, nil)
	handler.SetGitoWebhookConfig(cfg)
	handler.SetGitoBranchEventHandler(adapter)

	// 5. Build the signed request body.
	body := buildBranchUpdatedPayload(repoID, branch, before, after)

	// 6. Create the signed request.
	req := httptest.NewRequest(http.MethodPost, "/api/integrations/gito/webhook", bytes.NewReader(body))
	sig := signBody(secret, body)
	req.Header.Set("X-Gito-Signature", sig)
	req.Header.Set("X-Gito-Event", "branch.updated")
	req.Header.Set("X-Gito-Delivery", "delivery-1")

	w := httptest.NewRecorder()

	// 7. Call the handler directly.
	handler.ReceiveGitoWebhook(w, req)

	if w.Code != http.StatusAccepted {
		t.Fatalf("response status: got %d, want %d", w.Code, http.StatusAccepted)
	}

	// 8. Verify scanner received the event.
	if scanner.lastEvent.RepoID != repoID {
		t.Fatalf("scanner.RepoID: got %q, want %q", scanner.lastEvent.RepoID, repoID)
	}
	if scanner.lastEvent.Branch != branch {
		t.Fatalf("scanner.Branch: got %q, want %q", scanner.lastEvent.Branch, branch)
	}
	if scanner.lastEvent.After != after {
		t.Fatalf("scanner.After: got %q, want %q", scanner.lastEvent.After, after)
	}
	if scanner.lastEvent.Before != before {
		t.Fatalf("scanner.Before: got %q, want %q", scanner.lastEvent.Before, before)
	}

	// 9. Verify enqueuer received exactly one job.
	if len(enq.jobs) != 1 {
		t.Fatalf("enqueued jobs: got %d, want 1", len(enq.jobs))
	}

	job := enq.jobs[0]

	// 10. Verify job content.
	// RoadmapRevision should match the HTTP payload's `after` value (passed through scanner).
	if job.RoadmapRevision != after {
		t.Fatalf("RoadmapRevision: got %q, want %q (HTTP payload `after`)", job.RoadmapRevision, after)
	}
	if job.Expected.WorkItemID != "wi-123" {
		t.Fatalf("Expected.WorkItemID: got %q, want %q", job.Expected.WorkItemID, "wi-123")
	}
	if job.Ref.Provider != "plane" {
		t.Fatalf("Ref.Provider: got %q, want %q", job.Ref.Provider, "plane")
	}
	if job.Ref.ID != "wi-123" {
		t.Fatalf("Ref.ID: got %q, want %q", job.Ref.ID, "wi-123")
	}
	if job.OriginalBody != "<p>Original HTML body</p>" {
		t.Fatalf("OriginalBody: got %q, want HTML preferred", job.OriginalBody)
	}
}

// TestGitoHTTPWebhookDeliveryRejectedOnInvalidSignature verifies that an invalid
// signature results in a 401 Unauthorized response.
func TestGitoHTTPWebhookDeliveryRejectedOnInvalidSignature(t *testing.T) {
	const secret = "correct-secret"
	const repoID = "nomadcode"
	const branch = "develop"

	handler := apphttp.NewHandler(nil, nil, nil, nil)
	handler.SetGitoWebhookConfig(apphttp.GitoWebhookConfig{
		Secret: secret,
		RepoID: repoID,
		Branch: branch,
	})

	body := buildBranchUpdatedPayload(repoID, branch, "develop", "abc123")
	req := httptest.NewRequest(http.MethodPost, "/api/integrations/gito/webhook", bytes.NewReader(body))
	// Wrong secret.
	req.Header.Set("X-Gito-Signature", signBody("wrong-secret", body))
	req.Header.Set("X-Gito-Event", "branch.updated")
	req.Header.Set("X-Gito-Delivery", "delivery-invalid-signature")

	w := httptest.NewRecorder()
	handler.ReceiveGitoWebhook(w, req)

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

// TestGitoHTTPWebhookDeliveryOffTargetIgnored verifies that events for a
// different repo/branch receive 202 Accepted with status "ignored".
func TestGitoHTTPWebhookDeliveryOffTargetIgnored(t *testing.T) {
	const secret = "test-secret"
	const repoID = "nomadcode"
	const branch = "develop"

	handler := apphttp.NewHandler(nil, nil, nil, nil)
	handler.SetGitoWebhookConfig(apphttp.GitoWebhookConfig{
		Secret: secret,
		RepoID: repoID,
		Branch: branch,
	})

	body := buildBranchUpdatedPayload("other-repo", "main", "aaa", "bbb")
	sig := signBody(secret, body)
	req := httptest.NewRequest(http.MethodPost, "/api/integrations/gito/webhook", bytes.NewReader(body))
	req.Header.Set("X-Gito-Signature", sig)
	req.Header.Set("X-Gito-Event", "branch.updated")
	req.Header.Set("X-Gito-Delivery", "delivery-off-target")

	w := httptest.NewRecorder()
	handler.ReceiveGitoWebhook(w, req)

	if w.Code != http.StatusAccepted {
		t.Fatalf("response status: got %d, want %d", w.Code, http.StatusAccepted)
	}

	var resp map[string]string
	if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
		t.Fatalf("response parse: %v", err)
	}
	if resp["status"] != "ignored" {
		t.Fatalf("response status: got %q, want %q", resp["status"], "ignored")
	}
}
