package gitoevents

import (
	"strings"
	"testing"

	"github.com/nomadcode/nomadcode-core/internal/protosocket"
)

// branchUpdatedEnvelope builds a contract-shaped broadcast envelope fixture.
func branchUpdatedEnvelope() protosocket.Envelope {
	return protosocket.Envelope{
		ProtocolVersion: protosocket.ProtocolVersion,
		ID:              "event-id",
		Type:            "event",
		Channel:         EventChannel,
		Action:          BranchUpdatedAction,
		Payload: map[string]any{
			"id":          "event-id",
			"type":        "branch.updated",
			"provider":    "forgejo",
			"delivery_id": "delivery-id",
			"repo_id":     "nomadcode",
			"branch":      "develop",
			"before":      "old-sha",
			"after":       "new-sha",
			"changed_files": []any{
				map[string]any{
					"path":        "agent-roadmap/phase/example/milestones/example.md",
					"change_type": "modified",
				},
				map[string]any{
					"path":        "README.md",
					"change_type": "modified",
				},
			},
			"observed_at": "2026-06-13T00:00:00Z",
			"created_at":  "2026-06-13T00:00:00Z",
		},
	}
}

func TestBuildSubscribeEnvelopeMatchesContract(t *testing.T) {
	env := BuildSubscribeEnvelope("msg-1", "nomadcode", "develop")

	if env.ProtocolVersion != protosocket.ProtocolVersion {
		t.Fatalf("protocol_version: got %q", env.ProtocolVersion)
	}
	if env.ID != "msg-1" {
		t.Fatalf("id: got %q", env.ID)
	}
	if env.Type != "request" {
		t.Fatalf("type: got %q, want request", env.Type)
	}
	if env.Channel != EventChannel {
		t.Fatalf("channel: got %q, want %q", env.Channel, EventChannel)
	}
	if env.Action != SubscribeAction {
		t.Fatalf("action: got %q, want %q", env.Action, SubscribeAction)
	}
	if got := env.Payload["repo_id"]; got != "nomadcode" {
		t.Fatalf("payload.repo_id: got %v", got)
	}
	if got := env.Payload["branch"]; got != "develop" {
		t.Fatalf("payload.branch: got %v", got)
	}
	events, ok := env.Payload["events"].([]any)
	if !ok || len(events) != 1 || events[0] != BranchUpdatedAction {
		t.Fatalf("payload.events: got %v", env.Payload["events"])
	}
}

func TestBuildSubscribeEnvelopeDefaultsBranch(t *testing.T) {
	env := BuildSubscribeEnvelope("msg-1", "nomadcode", "")
	if got := env.Payload["branch"]; got != DefaultBranch {
		t.Fatalf("payload.branch default: got %v, want %q", got, DefaultBranch)
	}
}

func TestDecodeBranchUpdatedEnvelope(t *testing.T) {
	ev, err := DecodeBranchUpdatedEnvelope(branchUpdatedEnvelope())
	if err != nil {
		t.Fatalf("decode: unexpected error: %v", err)
	}
	if ev.RepoID != "nomadcode" || ev.Branch != "develop" {
		t.Fatalf("repo/branch: got %q/%q", ev.RepoID, ev.Branch)
	}
	if ev.Before != "old-sha" || ev.After != "new-sha" {
		t.Fatalf("before/after: got %q/%q", ev.Before, ev.After)
	}
	if ev.DeliveryID != "delivery-id" || ev.Provider != "forgejo" {
		t.Fatalf("delivery/provider: got %q/%q", ev.DeliveryID, ev.Provider)
	}
	if len(ev.ChangedFiles) != 2 {
		t.Fatalf("changed_files: got %d, want 2", len(ev.ChangedFiles))
	}
	if ev.ChangedFiles[0].Path != "agent-roadmap/phase/example/milestones/example.md" {
		t.Fatalf("changed_files[0].path: got %q", ev.ChangedFiles[0].Path)
	}
}

func TestDecodeBranchUpdatedEnvelopeAcceptsConsumerNeutralBasePayload(t *testing.T) {
	env := protosocket.Envelope{
		ProtocolVersion: protosocket.ProtocolVersion,
		ID:              "event-id",
		Type:            "event",
		Channel:         EventChannel,
		Action:          BranchUpdatedAction,
		Payload: map[string]any{
			"id":      "event-id",
			"type":    BranchUpdatedAction,
			"repo_id": "nomadcode",
			"branch":  "develop",
			"before":  "old-sha",
			"after":   "new-sha",
			"changed_files": []any{
				map[string]any{
					"path":        "agent-roadmap/phase/example/milestones/example.md",
					"change_type": "modified",
				},
			},
		},
	}

	ev, err := DecodeBranchUpdatedEnvelope(env)
	if err != nil {
		t.Fatalf("decode consumer-neutral payload: unexpected error: %v", err)
	}
	if ev.RepoID != "nomadcode" || ev.Branch != "develop" || ev.After != "new-sha" {
		t.Fatalf("decoded event: got %+v", ev)
	}
	if len(ev.MilestoneChangedFiles()) != 1 {
		t.Fatalf("milestone wakeup hint: got %+v", ev.MilestoneChangedFiles())
	}
}

func TestDecodeBranchUpdatedEnvelopeIgnoresOptionalConsumerExtensions(t *testing.T) {
	env := branchUpdatedEnvelope()
	env.Payload["metadata"] = map[string]any{
		"nomadcode": map[string]any{"ignored": true},
	}
	env.Payload["custom"] = map[string]any{
		"consumer": "nomadcode",
	}

	ev, err := DecodeBranchUpdatedEnvelope(env)
	if err != nil {
		t.Fatalf("decode with optional extensions: unexpected error: %v", err)
	}
	if ev.RepoID != "nomadcode" || ev.Branch != "develop" {
		t.Fatalf("decoded event: got %+v", ev)
	}
}

func TestDecodeBranchUpdatedEnvelopeRejectsWrongAction(t *testing.T) {
	env := branchUpdatedEnvelope()
	env.Action = "task.status.changed"
	if _, err := DecodeBranchUpdatedEnvelope(env); err == nil {
		t.Fatal("expected error for wrong action")
	}
}

func TestDecodeBranchUpdatedEnvelopeRejectsWrongChannel(t *testing.T) {
	env := branchUpdatedEnvelope()
	env.Channel = "task"
	if _, err := DecodeBranchUpdatedEnvelope(env); err == nil {
		t.Fatal("expected error for wrong channel")
	}
}

func TestDecodeBranchUpdatedEnvelopeRejectsMalformedChangedFiles(t *testing.T) {
	env := branchUpdatedEnvelope()
	env.Payload["changed_files"] = "not-a-list"
	if _, err := DecodeBranchUpdatedEnvelope(env); err == nil {
		t.Fatal("expected error for malformed changed_files")
	}
}

func TestDecodeBranchUpdatedEnvelopeRejectsMissingRepoID(t *testing.T) {
	env := branchUpdatedEnvelope()
	delete(env.Payload, "repo_id")
	if _, err := DecodeBranchUpdatedEnvelope(env); err == nil {
		t.Fatal("expected error for missing repo_id")
	}
}

func TestDecodeBranchUpdatedPayloadAcceptsBasePayloadWithoutNomadCodeFields(t *testing.T) {
	// Gito base JSON shape: minimal fields without NomadCode-specific extensions.
	payload := map[string]any{
		"id":       "event-123",
		"type":     "branch.updated",
		"provider": "forgejo",
		"repo_id":  "nomadcode",
		"branch":   "develop",
		"before":   "old-sha",
		"after":    "new-sha",
		"changed_files": []any{
			map[string]any{
				"path":        "agent-roadmap/phase/example/milestones/example.md",
				"change_type": "modified",
			},
			map[string]any{
				"path":        "README.md",
				"change_type": "modified",
			},
		},
		"observed_at": "2026-06-13T00:00:00Z",
		"created_at":  "2026-06-13T00:00:00Z",
	}

	ev, err := DecodeBranchUpdatedPayload(payload)
	if err != nil {
		t.Fatalf("decode base payload: unexpected error: %v", err)
	}
	if ev.RepoID != "nomadcode" || ev.Branch != "develop" || ev.After != "new-sha" {
		t.Fatalf("decoded event: got %+v", ev)
	}
	if ev.ID != "event-123" || ev.Provider != "forgejo" {
		t.Fatalf("id/provider: got %+v", ev)
	}
	if len(ev.ChangedFiles) != 2 {
		t.Fatalf("changed_files: got %d, want 2", len(ev.ChangedFiles))
	}
	if ev.DeliveryID != "" {
		t.Fatalf("delivery_id should be empty for minimal payload, got %q", ev.DeliveryID)
	}
}

func TestDecodeBranchUpdatedPayloadRejectsWrongType(t *testing.T) {
	payload := map[string]any{
		"id":      "event-456",
		"type":    "task.status.changed",
		"repo_id": "nomadcode",
		"branch":  "develop",
		"after":   "new-sha",
	}

	_, err := DecodeBranchUpdatedPayload(payload)
	if err == nil {
		t.Fatal("expected error for wrong type")
	}
	if !strings.Contains(err.Error(), "type") {
		t.Fatalf("error should mention type validation: %v", err)
	}
}

func TestDecodeBranchUpdatedPayloadRejectsMalformedChangedFiles(t *testing.T) {
	payload := map[string]any{
		"id":            "event-789",
		"type":          "branch.updated",
		"repo_id":       "nomadcode",
		"branch":        "develop",
		"after":         "new-sha",
		"changed_files": "not-a-list",
	}

	_, err := DecodeBranchUpdatedPayload(payload)
	if err == nil {
		t.Fatal("expected error for malformed changed_files")
	}
	if !strings.Contains(err.Error(), "changed_files") {
		t.Fatalf("error should mention changed_files: %v", err)
	}
}

func TestDecodeBranchUpdatedPayloadRejectsMissingRepoID(t *testing.T) {
	payload := map[string]any{
		"type":   "branch.updated",
		"branch": "develop",
		"after":  "new-sha",
	}

	_, err := DecodeBranchUpdatedPayload(payload)
	if err == nil {
		t.Fatal("expected error for missing repo_id")
	}
}

func TestDecodeBranchUpdatedPayloadRejectsMissingBranch(t *testing.T) {
	payload := map[string]any{
		"type":    "branch.updated",
		"repo_id": "nomadcode",
		"after":   "new-sha",
	}

	_, err := DecodeBranchUpdatedPayload(payload)
	if err == nil {
		t.Fatal("expected error for missing branch")
	}
}

func TestDecodeBranchUpdatedPayloadEmptyChangedFiles(t *testing.T) {
	payload := map[string]any{
		"id":            "event-empty",
		"type":          "branch.updated",
		"repo_id":       "nomadcode",
		"branch":        "develop",
		"after":         "new-sha",
		"changed_files": []any{},
	}

	ev, err := DecodeBranchUpdatedPayload(payload)
	if err != nil {
		t.Fatalf("decode empty changed_files: unexpected error: %v", err)
	}
	if ev.ChangedFiles == nil {
		t.Fatalf("changed_files should be empty slice, got nil")
	}
	if len(ev.ChangedFiles) != 0 {
		t.Fatalf("changed_files: got %d, want 0", len(ev.ChangedFiles))
	}
}

func TestDecodeBranchUpdatedPayloadNilPayload(t *testing.T) {
	_, err := DecodeBranchUpdatedPayload(nil)
	if err == nil {
		t.Fatal("expected error for nil payload")
	}
}

func TestDecodeBranchUpdatedPayloadAllowsNilChangedFiles(t *testing.T) {
	payload := map[string]any{
		"id":       "event-nil-cf",
		"type":     "branch.updated",
		"repo_id":  "nomadcode",
		"branch":   "develop",
		"after":    "new-sha",
		"provider": "forgejo",
	}

	ev, err := DecodeBranchUpdatedPayload(payload)
	if err != nil {
		t.Fatalf("decode with nil changed_files: unexpected error: %v", err)
	}
	if ev.ChangedFiles != nil {
		t.Fatalf("changed_files should be nil, got %+v", ev.ChangedFiles)
	}
}

func TestDecodeBranchUpdatedPayloadHandlesMalformedChangedFileItem(t *testing.T) {
	payload := map[string]any{
		"id":      "event-bad-item",
		"type":    "branch.updated",
		"repo_id": "nomadcode",
		"branch":  "develop",
		"after":   "new-sha",
		"changed_files": []any{
			"not-an-object",
		},
	}

	_, err := DecodeBranchUpdatedPayload(payload)
	if err == nil {
		t.Fatal("expected error for malformed changed_files item")
	}
	if !strings.Contains(err.Error(), "changed_files") {
		t.Fatalf("error should mention changed_files: %v", err)
	}
}

func TestDecodeBranchUpdatedPayloadWithoutTypeField(t *testing.T) {
	// type field is optional; when absent it should be accepted.
	payload := map[string]any{
		"id":       "event-no-type",
		"repo_id":  "nomadcode",
		"branch":   "develop",
		"after":    "new-sha",
		"provider": "forgejo",
	}

	ev, err := DecodeBranchUpdatedPayload(payload)
	if err != nil {
		t.Fatalf("decode without type: unexpected error: %v", err)
	}
	if ev.Type != "" {
		t.Fatalf("type should be empty when absent, got %q", ev.Type)
	}
	if ev.RepoID != "nomadcode" || ev.Branch != "develop" {
		t.Fatalf("repo/branch: got %+v", ev)
	}
}

func TestIsTarget(t *testing.T) {
	ev := BranchUpdatedEvent{RepoID: "nomadcode", Branch: "develop"}

	if !ev.IsTarget("nomadcode", "develop") {
		t.Fatal("expected match for same repo/branch")
	}
	if ev.IsTarget("other-repo", "develop") {
		t.Fatal("expected no match for different repo")
	}
	if ev.IsTarget("nomadcode", "main") {
		t.Fatal("expected no match for different branch")
	}
	if !ev.IsTarget("nomadcode", "") {
		t.Fatal("expected empty branch to default to develop and match")
	}
}

func TestMilestoneChangedFiles(t *testing.T) {
	ev := BranchUpdatedEvent{
		ChangedFiles: []ChangedFile{
			{Path: "agent-roadmap/phase/example/milestones/example.md", ChangeType: "modified"},
			{Path: "./agent-roadmap/phase/p/milestones/m.md", ChangeType: "added"},
			{Path: "README.md", ChangeType: "modified"},
			{Path: "agent-roadmap/phase/example/PHASE.md", ChangeType: "modified"},
			{Path: "agent-roadmap/phase/example/milestones/notes.txt", ChangeType: "modified"},
		},
	}

	got := ev.MilestoneChangedFiles()
	if len(got) != 2 {
		t.Fatalf("milestone changed files: got %d, want 2 (%+v)", len(got), got)
	}
	if got[0].Path != "agent-roadmap/phase/example/milestones/example.md" {
		t.Fatalf("milestone[0]: got %q", got[0].Path)
	}
	if got[1].Path != "./agent-roadmap/phase/p/milestones/m.md" {
		t.Fatalf("milestone[1]: got %q", got[1].Path)
	}
}
