package webhook

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"strings"
	"testing"
	"time"
	"unicode/utf8"

	"github.com/stretchr/testify/require"
)

func TestDeliveryKey(t *testing.T) {
	payload := []byte(`{"repository":"pingcap/agentteam"}`)
	require.Equal(t, "delivery-1", DeliveryKey(" delivery-1 ", "pull_request", payload))

	generated := DeliveryKey("", " pull_request ", payload)
	require.True(t, strings.HasPrefix(generated, "sha256:"))
	require.Equal(t, generated, DeliveryKey("", "pull_request", payload))
	require.NotEqual(t, generated, DeliveryKey("", "push", payload))
}

func TestGitHubWebhookHelpers(t *testing.T) {
	payload := []byte(`{"action":"closed","installation":{"id":123},"repository":{"full_name":"PingCAP/AgentTeam"},"pull_request":{"number":7,"merged":true}}`)
	secret := "github-secret"
	mac := hmac.New(sha256.New, []byte(secret))
	_, _ = mac.Write(payload)
	require.True(t, VerifyGitHubWebhookSignature("sha256="+hex.EncodeToString(mac.Sum(nil)), payload, secret))

	installationRef, ok := ExtractGitHubInstallationRef(payload)
	require.True(t, ok)
	require.Equal(t, "123", installationRef)

	event, parsed, err := ParseGitHubWebhookPayload(payload)
	require.NoError(t, err)
	require.True(t, parsed)
	require.Equal(t, "pull_request_closed", NormalizeGitHubWebhookEventType("pull_request", event.Action))

	resource, ok := GitHubPullRequestResourceFromPullRequest(event)
	require.True(t, ok)
	require.Equal(t, PullRequestResource{
		ResourceType:       "github_pull_request",
		ResourceIdentifier: "pingcap/agentteam#7",
		ResourceURL:        "https://github.com/pingcap/agentteam/pull/7",
		RepositoryFullName: "pingcap/agentteam",
		Number:             7,
	}, resource)
}

func TestProviderWebhookParsersReturnDecodeErrors(t *testing.T) {
	t.Parallel()

	_, parsed, err := ParseGitHubWebhookPayload([]byte(`{
	  "repository": {"full_name": "PingCAP/AgentTeam"},
	  "pull_request": {
	    "number": 7,
	    "updated_at": "not-a-timestamp"
	  }
	}`))
	require.Error(t, err)
	require.False(t, parsed)

	_, parsed, err = ParseGitHubWebhookPayload([]byte(`{"installation":{"id":123}} trailing`))
	require.Error(t, err)
	require.False(t, parsed)

	_, parsed, err = ParseGiteaWebhookPayload([]byte(`{
	  "repository": {"full_name": "PingCAP/AgentTeam"},
	  "pull_request": {
	    "number": 7,
	    "updated_at": "not-a-timestamp"
	  }
	}`))
	require.Error(t, err)
	require.False(t, parsed)

	repoRef, _, ok := ExtractGiteaConnectionRefs([]byte(`{"repository":{"full_name":"PingCAP/AgentTeam"}} trailing`))
	require.True(t, ok)
	require.Equal(t, "pingcap/agentteam", repoRef)

	_, parsed, err = ParseGiteaWebhookPayload([]byte(`{"repository":{"full_name":"PingCAP/AgentTeam"}} trailing`))
	require.Error(t, err)
	require.False(t, parsed)
}

func TestGitHubIssueCommentRequiresPullRequestMarker(t *testing.T) {
	payload := []byte(`{"repository":{"full_name":"PingCAP/AgentTeam"},"issue":{"number":8,"html_url":"https://github.com/PingCAP/AgentTeam/issues/8","pull_request":{"html_url":"https://github.com/PingCAP/AgentTeam/pull/8"}}}`)
	event, parsed, err := ParseGitHubWebhookPayload(payload)
	require.NoError(t, err)
	require.True(t, parsed)

	resource, ok, isPR := GitHubPullRequestResourceFromIssueComment(event)
	require.True(t, isPR)
	require.True(t, ok)
	require.Equal(t, "pingcap/agentteam#8", resource.ResourceIdentifier)

	event.Issue.PullRequest = nil
	_, ok, isPR = GitHubPullRequestResourceFromIssueComment(event)
	require.False(t, isPR)
	require.False(t, ok)
}

func TestProviderEventBodyExcerptIsByteCapped(t *testing.T) {
	t.Parallel()

	longBody := strings.Repeat("a", 5000)
	payload, err := json.Marshal(map[string]any{
		"action":     "created",
		"repository": map[string]any{"full_name": "PingCAP/AgentTeam"},
		"issue": map[string]any{
			"number":       8,
			"html_url":     "https://github.com/PingCAP/AgentTeam/issues/8",
			"pull_request": map[string]any{"html_url": "https://github.com/PingCAP/AgentTeam/pull/8"},
		},
		"comment": map[string]any{
			"body":     longBody,
			"html_url": "https://github.com/PingCAP/AgentTeam/issues/8#issuecomment-1",
			"user":     map[string]any{"login": "reviewer"},
		},
	})
	require.NoError(t, err)

	event, parsed, err := ParseGitHubWebhookPayload(payload)
	require.NoError(t, err)
	require.True(t, parsed)

	normalized, ok, err := NormalizeGitHubProviderEvent("ws_1", "789", "delivery-1", "issue_comment_created", event, time.Now().UTC())
	require.NoError(t, err)
	require.True(t, ok)
	require.LessOrEqual(t, len(normalized.BodyExcerpt), 4096)
	require.Less(t, len(normalized.BodyExcerpt), len(longBody))
}

func TestTruncateProviderTextKeepsValidUTF8ForSmallLimits(t *testing.T) {
	t.Parallel()

	for _, limit := range []int{1, 2, 3, 4} {
		truncated := TruncateProviderText("你好世界", limit)
		require.LessOrEqual(t, len(truncated), limit)
		require.True(t, utf8.ValidString(truncated), "limit %d produced invalid UTF-8: %q", limit, truncated)
	}
}

func TestGiteaWebhookHelpers(t *testing.T) {
	payload := []byte(`{"action":"synchronized","repository":{"full_name":"PingCAP/AgentTeam","name":"AgentTeam","owner":{"login":"PingCAP"}},"pull_request":{"number":5,"base":{"ref":"main","sha":"base-sha"},"head":{"ref":"feature","sha":"head-sha"}}}`)
	secret := "gitea-secret"
	mac := hmac.New(sha256.New, []byte(secret))
	_, _ = mac.Write(payload)
	require.True(t, VerifyGiteaWebhookSignature("sha256="+strings.ToUpper(hex.EncodeToString(mac.Sum(nil))), payload, secret))

	repoRef, orgRef, ok := ExtractGiteaConnectionRefs(payload)
	require.True(t, ok)
	require.Equal(t, "pingcap/agentteam", repoRef)
	require.Equal(t, "pingcap", orgRef)

	event, parsed, err := ParseGiteaWebhookPayload(payload)
	require.NoError(t, err)
	require.True(t, parsed)
	require.Equal(t, "pull_request_synchronize", NormalizeGiteaWebhookEventType("pull_request", event.Action))

	resource, ok := GiteaPullRequestResourceFromPullRequest(event, "https://gitea.example.com/git/")
	require.True(t, ok)
	require.Equal(t, PullRequestResource{
		ResourceType:       "gitea_pull_request",
		ResourceIdentifier: "pingcap/agentteam#5",
		ResourceURL:        "https://gitea.example.com/git/pingcap/agentteam/pulls/5",
		RepositoryFullName: "pingcap/agentteam",
		Number:             5,
	}, resource)
}

func TestGiteaPushWebhookNormalizesBaseBranchEvent(t *testing.T) {
	payload := []byte(`{
	  "ref": "refs/heads/main",
	  "before": "old-sha",
	  "after": "new-sha",
	  "commits": [
	    {"added": ["cmd/server/main.go"], "modified": ["README.md"], "removed": ["old.txt"]},
	    {"modified": ["README.md", "docs/deploy.md"]}
	  ],
	  "compare_url": "https://gitea.example.com/pingcap/agentteam/compare/old-sha...new-sha",
	  "repository": {
	    "full_name": "PingCAP/AgentTeam",
	    "name": "AgentTeam",
	    "default_branch": "main",
	    "owner": {"login": "PingCAP"}
	  },
	  "sender": {"login": "pusher"}
	}`)

	event, parsed, err := ParseGiteaWebhookPayload(payload)
	require.NoError(t, err)
	require.True(t, parsed)
	require.Equal(t, "push", NormalizeGiteaWebhookEventType("push", event.Action))
	require.Equal(t, "main", providerPushBranchFromRef(event.Ref))
	require.Empty(t, providerPushBranchFromRef("refs/tags/v1.0.0"))

	normalized, ok, err := NormalizeGiteaProviderEvent("wsp_1", "pingcap/agentteam", "delivery-1", "push", event, time.Date(2026, 5, 18, 10, 0, 0, 0, time.UTC), "https://gitea.example.com")
	require.NoError(t, err)
	require.True(t, ok)
	require.Equal(t, "gitea_pull_request.base_branch_pushed", normalized.EventKind)
	require.Equal(t, "base_branch_pushed", normalized.ActionKind)
	require.Equal(t, "gitea:pingcap/agentteam", normalized.ResourceIdentifier)
	require.Equal(t, "main", normalized.Extra["branch"])
	require.Equal(t, "main", normalized.Extra["default_branch"])
	require.Equal(t, "new-sha", normalized.Extra["after"])
	require.Equal(t, []string{"README.md", "cmd/server/main.go", "docs/deploy.md", "old.txt"}, normalized.ChangedPaths)
}

func TestGiteaPullRequestSynchronizeWebhookNormalizesHeadSHA(t *testing.T) {
	payload := []byte(`{
	  "action": "synchronized",
	  "repository": {"full_name": "PingCAP/AgentTeam"},
	  "pull_request": {
	    "number": 5,
	    "html_url": "https://gitea.example.com/pingcap/agentteam/pulls/5",
	    "base": {"ref": "main", "sha": "base-sha"},
	    "head": {"ref": "feature", "sha": "head-sha"}
	  },
	  "sender": {"login": "ci-bot"}
	}`)

	event, parsed, err := ParseGiteaWebhookPayload(payload)
	require.NoError(t, err)
	require.True(t, parsed)

	normalized, ok, err := NormalizeGiteaProviderEvent("wsp_1", "pingcap/agentteam", "delivery-sync", "pull_request_synchronize", event, time.Date(2026, 5, 18, 12, 0, 0, 0, time.UTC), "https://gitea.example.com")
	require.NoError(t, err)
	require.True(t, ok)
	require.Equal(t, "gitea_pull_request.synchronized", normalized.EventKind)
	require.Equal(t, "synchronized", normalized.ActionKind)
	require.Equal(t, "gitea:pingcap/agentteam#5", normalized.ResourceIdentifier)
	require.Equal(t, "head-sha", normalized.Extra["head_sha"])
	require.Equal(t, "feature", normalized.Extra["head_branch"])
	require.Equal(t, "base-sha", normalized.Extra["base_sha"])
}

func TestGitHubPullRequestSynchronizeWebhookNormalizesHeadSHA(t *testing.T) {
	payload := []byte(`{
	  "action": "synchronize",
	  "repository": {"full_name": "PingCAP/AgentTeam"},
	  "pull_request": {
	    "number": 5,
	    "html_url": "https://github.com/pingcap/agentteam/pull/5",
	    "head": {"sha": "head-sha"}
	  },
	  "sender": {"login": "ci-bot"}
	}`)

	event, parsed, err := ParseGitHubWebhookPayload(payload)
	require.NoError(t, err)
	require.True(t, parsed)

	normalized, ok, err := NormalizeGitHubProviderEvent("wsp_1", "789", "delivery-sync", "pull_request_synchronize", event, time.Date(2026, 5, 18, 12, 0, 0, 0, time.UTC))
	require.NoError(t, err)
	require.True(t, ok)
	require.Equal(t, "github_pull_request.synchronized", normalized.EventKind)
	require.Equal(t, "synchronized", normalized.ActionKind)
	require.Equal(t, "github:pingcap/agentteam#5", normalized.ResourceIdentifier)
	require.Equal(t, "head-sha", normalized.Extra["head_sha"])
}

func TestGitHubPushWebhookNormalizesBaseBranchEvent(t *testing.T) {
	payload := []byte(`{
	  "ref": "refs/heads/main",
	  "before": "old-sha",
	  "after": "new-sha",
	  "commits": [
	    {"added": ["cmd/server/main.go"], "modified": ["README.md"], "removed": ["old.txt"]},
	    {"modified": ["README.md", "docs/deploy.md"]}
	  ],
	  "compare": "https://github.com/PingCAP/AgentTeam/compare/old-sha...new-sha",
	  "repository": {
	    "full_name": "PingCAP/AgentTeam",
	    "default_branch": "main"
	  },
	  "sender": {"login": "pusher"}
	}`)

	event, parsed, err := ParseGitHubWebhookPayload(payload)
	require.NoError(t, err)
	require.True(t, parsed)
	require.Equal(t, "push", NormalizeGitHubWebhookEventType("push", event.Action))

	normalized, ok, err := NormalizeGitHubProviderEvent("wsp_1", "789", "delivery-1", "push", event, time.Date(2026, 5, 18, 10, 0, 0, 0, time.UTC))
	require.NoError(t, err)
	require.True(t, ok)
	require.Equal(t, "github_pull_request.base_branch_pushed", normalized.EventKind)
	require.Equal(t, "base_branch_pushed", normalized.ActionKind)
	require.Equal(t, "github:pingcap/agentteam", normalized.ResourceIdentifier)
	require.Equal(t, "main", normalized.Extra["branch"])
	require.Equal(t, "main", normalized.Extra["default_branch"])
	require.Equal(t, "new-sha", normalized.Extra["after"])
	require.Equal(t, []string{"README.md", "cmd/server/main.go", "docs/deploy.md", "old.txt"}, normalized.ChangedPaths)
}

func TestGiteaStatusWebhookNormalizesCIStatusEvent(t *testing.T) {
	payload := []byte(`{
	  "sha": "head-sha",
	  "state": "failure",
	  "context": "ci/build",
	  "target_url": "https://ci.example.com/build/1",
	  "description": "build failed",
	  "updated_at": "2026-05-18T11:12:13Z",
	  "repository": {"full_name": "PingCAP/AgentTeam"},
	  "sender": {"login": "ci-bot"}
	}`)

	event, parsed, err := ParseGiteaWebhookPayload(payload)
	require.NoError(t, err)
	require.True(t, parsed)
	require.Equal(t, "status", NormalizeGiteaWebhookEventType("status", event.Action))

	fallback := time.Date(2026, 5, 18, 12, 0, 0, 0, time.UTC)
	normalized, ok, err := NormalizeGiteaProviderEvent("wsp_1", "pingcap/agentteam", "delivery-2", "status", event, fallback, "https://gitea.example.com")
	require.NoError(t, err)
	require.True(t, ok)
	require.Equal(t, "gitea_pull_request.ci_status", normalized.EventKind)
	require.Equal(t, "failure", normalized.ActionKind)
	require.Equal(t, "gitea:pingcap/agentteam", normalized.ResourceIdentifier)
	require.Equal(t, "https://ci.example.com/build/1", normalized.ResourceURL)
	require.Equal(t, "head-sha", normalized.Extra["sha"])
	require.Equal(t, "ci/build", normalized.Extra["context"])
	require.Equal(t, "build failed", normalized.BodyExcerpt)
	require.Equal(t, time.Date(2026, 5, 18, 11, 12, 13, 0, time.UTC), normalized.OccurredAt)
}

func TestGiteaStatusWebhookSkipsNonFailedCIStatusEvent(t *testing.T) {
	payload := []byte(`{
	  "sha": "head-sha",
	  "state": "success",
	  "context": "ci/build",
	  "target_url": "https://ci.example.com/build/1",
	  "description": "build passed",
	  "updated_at": "2026-05-18T11:12:13Z",
	  "repository": {"full_name": "PingCAP/AgentTeam"},
	  "sender": {"login": "ci-bot"}
	}`)

	event, parsed, err := ParseGiteaWebhookPayload(payload)
	require.NoError(t, err)
	require.True(t, parsed)

	normalized, ok, err := NormalizeGiteaProviderEvent("wsp_1", "pingcap/agentteam", "delivery-success", "status", event, time.Date(2026, 5, 18, 12, 0, 0, 0, time.UTC), "https://gitea.example.com")
	require.NoError(t, err)
	require.False(t, ok)
	require.Empty(t, normalized.EventKind)
}

func TestGiteaWorkflowWebhookNormalizesPullRequestCIEvent(t *testing.T) {
	payload := []byte(`{
	  "action": "completed",
	  "workflow": {"name": "CI"},
	  "workflow_run": {
	    "id": 1,
	    "name": "build",
	    "status": "completed",
	    "conclusion": "failure",
	    "html_url": "https://gitea.example.com/pingcap/agentteam/actions/runs/1",
	    "head_sha": "head-sha"
	  },
	  "repository": {"full_name": "PingCAP/AgentTeam"},
	  "pull_request": {"number": 5, "html_url": "https://gitea.example.com/pingcap/agentteam/pulls/5"},
	  "sender": {"login": "gitea-actions"}
	}`)

	event, parsed, err := ParseGiteaWebhookPayload(payload)
	require.NoError(t, err)
	require.True(t, parsed)
	require.Equal(t, "workflow_run", NormalizeGiteaWebhookEventType("workflow_run", event.Action))

	normalized, ok, err := NormalizeGiteaProviderEvent("wsp_1", "pingcap/agentteam", "delivery-3", "workflow_run", event, time.Date(2026, 5, 18, 12, 0, 0, 0, time.UTC), "https://gitea.example.com")
	require.NoError(t, err)
	require.True(t, ok)
	require.Equal(t, "gitea_pull_request.workflow_run_status", normalized.EventKind)
	require.Equal(t, "failure", normalized.ActionKind)
	require.Equal(t, "gitea:pingcap/agentteam#5", normalized.ResourceIdentifier)
	require.Equal(t, "https://gitea.example.com/pingcap/agentteam/actions/runs/1", normalized.ResourceURL)
	require.Equal(t, "CI", normalized.Extra["workflow"])
	require.Equal(t, "build", normalized.Extra["name"])
	require.Equal(t, "head-sha", normalized.Extra["head_sha"])
}

func TestGiteaWorkflowWebhookSkipsNonFailedPullRequestCIEvent(t *testing.T) {
	payload := []byte(`{
	  "action": "completed",
	  "workflow": {"name": "CI"},
	  "workflow_run": {
	    "id": 1,
	    "name": "build",
	    "status": "completed",
	    "conclusion": "success",
	    "html_url": "https://gitea.example.com/pingcap/agentteam/actions/runs/1",
	    "head_sha": "head-sha"
	  },
	  "repository": {"full_name": "PingCAP/AgentTeam"},
	  "pull_request": {"number": 5, "html_url": "https://gitea.example.com/pingcap/agentteam/pulls/5"},
	  "sender": {"login": "gitea-actions"}
	}`)

	event, parsed, err := ParseGiteaWebhookPayload(payload)
	require.NoError(t, err)
	require.True(t, parsed)

	normalized, ok, err := NormalizeGiteaProviderEvent("wsp_1", "pingcap/agentteam", "delivery-success", "workflow_run", event, time.Date(2026, 5, 18, 12, 0, 0, 0, time.UTC), "https://gitea.example.com")
	require.NoError(t, err)
	require.False(t, ok)
	require.Empty(t, normalized.EventKind)
}

func TestGiteaHelpersNormalizeURLsAndReviews(t *testing.T) {
	require.Equal(t, []string{"git", "org"}, SplitPathSegments(" /git//org/ "))
	require.Equal(t, "https://gitea.example.com/git/pingcap/agentteam/pulls/42", BuildGiteaPullRequestURL("https://gitea.example.com/git?x=1#frag", "pingcap/agentteam", 42))
	require.Empty(t, BuildGiteaPullRequestURL("not a url", "pingcap/agentteam", 42))
	require.Equal(t, "changes_requested", NormalizeGiteaReviewState("request_changes"))
}

func TestStringValue(t *testing.T) {
	require.Equal(t, "agentteam", StringValue(" agentteam "))
	require.Empty(t, StringValue(nil))
	require.Empty(t, StringValue(42))
}

func TestNormalizeGitHubProviderEventUsesProviderTimestamp(t *testing.T) {
	t.Parallel()

	payload := []byte(`{
	  "action": "submitted",
	  "repository": {"full_name": "Acme/AgentTeam"},
	  "pull_request": {
	    "number": 42,
	    "html_url": "https://github.com/acme/agentteam/pull/42",
	    "updated_at": "2026-04-19T10:10:00Z"
	  },
	  "review": {
	    "state": "approved",
	    "body": "LGTM",
	    "html_url": "https://github.com/acme/agentteam/pull/42#pullrequestreview-1",
	    "submitted_at": "2026-04-19T10:12:13Z",
	    "user": {"login": "reviewer"}
	  }
	}`)

	event, parsed, err := ParseGitHubWebhookPayload(payload)
	require.NoError(t, err)
	require.True(t, parsed)

	fallback := time.Date(2026, 4, 19, 12, 0, 0, 0, time.UTC)
	normalized, ok, err := NormalizeGitHubProviderEvent("ws_123", "789", "delivery-1", "pull_request_review_submitted", event, fallback)
	require.NoError(t, err)
	require.True(t, ok)
	require.Equal(t, "github_pull_request.review_submitted", normalized.EventKind)
	require.Equal(t, "approved", normalized.ActionKind)
	require.Equal(t, time.Date(2026, 4, 19, 10, 12, 13, 0, time.UTC), normalized.OccurredAt)
}

func TestNormalizeGiteaProviderEventUsesCommentTimestamp(t *testing.T) {
	t.Parallel()

	payload := []byte(`{
	  "action": "created",
	  "repository": {"full_name": "Acme/AgentTeam"},
	  "issue": {
	    "number": 42,
	    "html_url": "https://gitea.example.com/acme/agentteam/issues/42",
	    "pull_request": {"html_url": "https://gitea.example.com/acme/agentteam/pulls/42"}
	  },
	  "comment": {
	    "id": 1,
	    "body": "Please fix this",
	    "html_url": "https://gitea.example.com/acme/agentteam/issues/42#issuecomment-1",
	    "created_at": "2026-04-19T09:08:07Z",
	    "user": {"login": "reviewer"}
	  }
	}`)

	event, parsed, err := ParseGiteaWebhookPayload(payload)
	require.NoError(t, err)
	require.True(t, parsed)

	fallback := time.Date(2026, 4, 19, 12, 0, 0, 0, time.UTC)
	normalized, ok, err := NormalizeGiteaProviderEvent("ws_123", "acme", "delivery-2", "issue_comment_created", event, fallback, "https://gitea.example.com")
	require.NoError(t, err)
	require.True(t, ok)
	require.Equal(t, "gitea_pull_request.issue_comment_created", normalized.EventKind)
	require.Equal(t, "created", normalized.ActionKind)
	require.Equal(t, time.Date(2026, 4, 19, 9, 8, 7, 0, time.UTC), normalized.OccurredAt)
}

func TestNormalizeGitHubProviderEventFallsBackToReceiveTime(t *testing.T) {
	t.Parallel()

	payload := []byte(`{
	  "action": "reopened",
	  "repository": {"full_name": "Acme/AgentTeam"},
	  "pull_request": {
	    "number": 42,
	    "html_url": "https://github.com/acme/agentteam/pull/42"
	  },
	  "sender": {"login": "maintainer"}
	}`)

	event, parsed, err := ParseGitHubWebhookPayload(payload)
	require.NoError(t, err)
	require.True(t, parsed)

	fallback := time.Date(2026, 4, 19, 12, 34, 56, 0, time.UTC)
	normalized, ok, err := NormalizeGitHubProviderEvent("ws_123", "789", "delivery-3", "pull_request_reopened", event, fallback)
	require.NoError(t, err)
	require.True(t, ok)
	require.Equal(t, fallback, normalized.OccurredAt)
}
