package webhook

import (
	"net/http"
	"strings"
	"testing"
)

func headerWith(k, v string) http.Header {
	h := http.Header{}
	h.Set(k, v)
	return h
}

func TestParseGiteaPackageEvent_Container(t *testing.T) {
	body := []byte(`{
		"action": "created",
		"package": {
			"name": "my-app",
			"type": "container",
			"version": "v1.2.3",
			"owner": {"login": "alexei"},
			"repository": {"full_name": "alexei/my-app", "html_url": "https://git.example.com/alexei/my-app"}
		}
	}`)
	res := parseGiteaPackageEvent(body, headerWith(headerGiteaEvent, "package"))
	if !res.ok {
		t.Fatalf("expected ok=true")
	}
	if res.err != nil {
		t.Fatalf("unexpected err: %v", res.err)
	}
	if res.event.Kind != "image-push" || res.event.Image == nil {
		t.Fatalf("kind=%q image=%+v", res.event.Kind, res.event.Image)
	}
	if res.event.Image.Repo != "alexei/my-app" {
		t.Errorf("Repo=%q want alexei/my-app", res.event.Image.Repo)
	}
	if res.event.Image.Tag != "v1.2.3" {
		t.Errorf("Tag=%q want v1.2.3", res.event.Image.Tag)
	}
	if res.event.Image.Registry != "git.example.com" {
		t.Errorf("Registry=%q want git.example.com", res.event.Image.Registry)
	}
}

func TestParseGiteaPackageEvent_NonContainerRejected(t *testing.T) {
	body := []byte(`{"action": "created", "package": {"name": "lib", "type": "npm", "version": "1.0.0"}}`)
	res := parseGiteaPackageEvent(body, headerWith(headerGiteaEvent, "package"))
	if !res.ok {
		t.Fatalf("expected ok=true (claimed)")
	}
	if res.err == nil || !strings.Contains(res.err.Error(), "container") {
		t.Errorf("expected container-type error, got %v", res.err)
	}
}

func TestParseGiteaPackageEvent_NoHeaderSkips(t *testing.T) {
	res := parseGiteaPackageEvent([]byte(`{}`), http.Header{})
	if res.ok {
		t.Errorf("expected ok=false when header missing")
	}
}

func TestParseGitHubPackageEvent_RegistryPackage(t *testing.T) {
	body := []byte(`{
		"action": "published",
		"registry_package": {
			"name": "my-app",
			"namespace": "owner",
			"package_type": "CONTAINER",
			"package_version": {
				"name": "sha256:deadbeef",
				"container_metadata": {"tag": {"name": "v2.0.0"}}
			},
			"registry": {"url": "https://ghcr.io/owner/my-app"}
		}
	}`)
	res := parseGitHubPackageEvent(body, headerWith(headerGitHubEvent, "registry_package"))
	if !res.ok || res.err != nil {
		t.Fatalf("ok=%v err=%v", res.ok, res.err)
	}
	if res.event.Image == nil || res.event.Image.Tag != "v2.0.0" {
		t.Errorf("Tag mismatch: %+v", res.event.Image)
	}
	if res.event.Image.Digest != "sha256:deadbeef" {
		t.Errorf("Digest=%q want sha256:deadbeef", res.event.Image.Digest)
	}
	if res.event.Image.Repo != "owner/my-app" {
		t.Errorf("Repo=%q want owner/my-app", res.event.Image.Repo)
	}
	if res.event.Image.Registry != "ghcr.io" {
		t.Errorf("Registry=%q want ghcr.io", res.event.Image.Registry)
	}
}

func TestParseGitHubPackageEvent_PackageAlias(t *testing.T) {
	// Older webhooks deliver under "package" with event name "package".
	body := []byte(`{
		"action": "published",
		"package": {
			"name": "img",
			"namespace": "org",
			"package_type": "container",
			"package_version": {"container_metadata": {"tag": {"name": "latest"}}},
			"registry": {"url": "https://ghcr.io/"}
		}
	}`)
	res := parseGitHubPackageEvent(body, headerWith(headerGitHubEvent, "package"))
	if !res.ok || res.err != nil {
		t.Fatalf("ok=%v err=%v", res.ok, res.err)
	}
	if res.event.Image.Repo != "org/img" {
		t.Errorf("Repo=%q want org/img", res.event.Image.Repo)
	}
	if res.event.Image.Tag != "latest" {
		t.Errorf("Tag=%q want latest", res.event.Image.Tag)
	}
}

func TestParseGitHubPushEvent_StampsVendor(t *testing.T) {
	body := []byte(`{
		"ref": "refs/heads/main",
		"after": "abc",
		"repository": {"full_name": "owner/repo"},
		"pusher": {"name": "alice"}
	}`)
	res := parseGitHubPushEvent(body, headerWith(headerGitHubEvent, "push"))
	if !res.ok || res.err != nil {
		t.Fatalf("ok=%v err=%v", res.ok, res.err)
	}
	if res.event.Git == nil || res.event.Git.Vendor != "github" {
		t.Errorf("Vendor=%q want github (git=%+v)", "", res.event.Git)
	}
	if res.event.Git.Branch != "main" {
		t.Errorf("Branch=%q want main", res.event.Git.Branch)
	}
}

func TestParseGiteaPushEvent_StampsVendor(t *testing.T) {
	body := []byte(`{
		"ref": "refs/tags/v1.0",
		"after": "deadbeef",
		"repository": {"full_name": "alexei/app"},
		"pusher": {"username": "alexei"}
	}`)
	res := parseGiteaPushEvent(body, headerWith(headerGiteaEvent, "push"))
	if !res.ok || res.err != nil {
		t.Fatalf("ok=%v err=%v", res.ok, res.err)
	}
	if res.event.Kind != "git-tag" {
		t.Errorf("Kind=%q want git-tag", res.event.Kind)
	}
	if res.event.Git.Vendor != "gitea" {
		t.Errorf("Vendor=%q want gitea", res.event.Git.Vendor)
	}
	if res.event.Git.Tag != "v1.0" {
		t.Errorf("Tag=%q want v1.0", res.event.Git.Tag)
	}
	if res.event.Git.Pusher != "alexei" {
		t.Errorf("Pusher=%q want alexei", res.event.Git.Pusher)
	}
}

func TestParseGitLabPushEvent(t *testing.T) {
	body := []byte(`{
		"ref": "refs/heads/develop",
		"after": "feedface",
		"user_username": "bob",
		"project": {"path_with_namespace": "group/proj", "git_http_url": "https://gitlab.example.com/group/proj.git"}
	}`)
	res := parseGitLabPushEvent(body, headerWith(headerGitLabEvent, "Push Hook"))
	if !res.ok || res.err != nil {
		t.Fatalf("ok=%v err=%v", res.ok, res.err)
	}
	if res.event.Git.Vendor != "gitlab" {
		t.Errorf("Vendor=%q want gitlab", res.event.Git.Vendor)
	}
	if res.event.Git.Repo != "group/proj" {
		t.Errorf("Repo=%q want group/proj", res.event.Git.Repo)
	}
	if res.event.Git.Branch != "develop" {
		t.Errorf("Branch=%q want develop", res.event.Git.Branch)
	}
	if res.event.Git.Pusher != "bob" {
		t.Errorf("Pusher=%q want bob", res.event.Git.Pusher)
	}
}

func TestParseGitLabTagPushEvent(t *testing.T) {
	body := []byte(`{
		"ref": "refs/tags/v3",
		"after": "abc",
		"user_name": "carol",
		"project": {"path_with_namespace": "g/p"}
	}`)
	res := parseGitLabPushEvent(body, headerWith(headerGitLabEvent, "Tag Push Hook"))
	if !res.ok || res.err != nil {
		t.Fatalf("ok=%v err=%v", res.ok, res.err)
	}
	if res.event.Kind != "git-tag" || res.event.Git.Tag != "v3" {
		t.Errorf("Tag mismatch: kind=%q git=%+v", res.event.Kind, res.event.Git)
	}
	if res.event.Git.Pusher != "carol" {
		t.Errorf("Pusher=%q want carol (user_name fallback)", res.event.Git.Pusher)
	}
}

func TestBuildInboundEvent_GiteaPackageRouted(t *testing.T) {
	body := []byte(`{
		"action": "created",
		"package": {
			"name": "svc",
			"type": "container",
			"version": "v9",
			"owner": {"login": "alexei"},
			"repository": {"html_url": "https://git.example.com/alexei/svc"}
		}
	}`)
	evt, err := buildInboundEvent(body, headerWith(headerGiteaEvent, "package"))
	if err != nil {
		t.Fatalf("unexpected err: %v", err)
	}
	if evt.Image == nil || evt.Image.Tag != "v9" {
		t.Errorf("expected Gitea-routed image-push, got %+v", evt)
	}
	// RawBody and Headers should round-trip even via the vendor branch.
	if len(evt.RawBody) == 0 {
		t.Errorf("RawBody should be attached")
	}
	if http.Header(evt.Headers).Get(headerGiteaEvent) != "package" {
		t.Errorf("Headers not attached")
	}
}

func TestBuildInboundEvent_FallbackToGeneric(t *testing.T) {
	// No vendor header — generic simple-body parser still works.
	body := []byte(`{"image":"reg.example.com/x/y:tag"}`)
	evt, err := buildInboundEvent(body, http.Header{})
	if err != nil {
		t.Fatalf("unexpected err: %v", err)
	}
	if evt.Image == nil || evt.Image.Tag != "tag" {
		t.Errorf("generic fallback failed: %+v", evt)
	}
}

func TestBuildInboundEvent_GenericRefStillSupported(t *testing.T) {
	body := []byte(`{"ref":"refs/heads/main","repository":{"full_name":"a/b"},"after":"sha"}`)
	evt, err := buildInboundEvent(body, http.Header{})
	if err != nil {
		t.Fatalf("unexpected err: %v", err)
	}
	if evt.Kind != "git-push" || evt.Git == nil || evt.Git.Branch != "main" {
		t.Errorf("generic ref parse failed: %+v", evt)
	}
	// Generic path does NOT stamp a vendor — only the vendor-header
	// paths do, so trigger plugins can tell them apart.
	if evt.Git.Vendor != "" {
		t.Errorf("generic parser should leave Vendor empty, got %q", evt.Git.Vendor)
	}
}

func TestBuildInboundEvent_VendorErrorDoesNotFallThrough(t *testing.T) {
	// A request with the Gitea header but a non-container package should
	// error out cleanly rather than fall through to the generic parser
	// (which would silently accept the body as JSON-with-no-fields).
	body := []byte(`{"package":{"name":"x","type":"npm","version":"1.0"}}`)
	_, err := buildInboundEvent(body, headerWith(headerGiteaEvent, "package"))
	if err == nil {
		t.Fatal("expected error for non-container Gitea package")
	}
	if !strings.Contains(err.Error(), "container") {
		t.Errorf("error should mention container, got %v", err)
	}
}

func TestRegistryHostFromGiteaRepoURL(t *testing.T) {
	cases := []struct{ in, out string }{
		{"https://git.example.com/owner/repo", "git.example.com"},
		{"http://localhost:3000/o/r", "localhost:3000"},
		{"git.example.com/owner/repo", "git.example.com"},
		{"", ""},
	}
	for _, c := range cases {
		got := registryHostFromGiteaRepoURL(c.in)
		if got != c.out {
			t.Errorf("registryHostFromGiteaRepoURL(%q) = %q, want %q", c.in, got, c.out)
		}
	}
}
