package github

import (
	"context"
	"encoding/json"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	reposync "github.com/pingcap/agent-git-service/internal/sync"
	"github.com/pingcap/agent-git-service/internal/sync/canonical"
	"github.com/pingcap/agent-git-service/internal/sync/githubapp"
)

type discoveryTokenSource struct{}

func (discoveryTokenSource) APIURL() string { return "https://api.github.test" }
func (discoveryTokenSource) Token(context.Context, string) (githubapp.InstallationToken, error) {
	return githubapp.InstallationToken{Value: "token", ExpiresAt: time.Now().Add(time.Hour), Permissions: map[string]string{
		"contents": "write", "issues": "write", "pull_requests": "write", "workflows": "write", "metadata": "read",
	}}, nil
}
func (discoveryTokenSource) ListInstallations(context.Context) ([]githubapp.Installation, error) {
	return []githubapp.Installation{{ID: 9, AccountLogin: "pingcap", AccountType: "Organization", HTMLURL: "https://github.com/settings/installations/9", RepositorySelection: "selected", Permissions: map[string]string{
		"contents": "write", "issues": "write", "pull_requests": "write", "workflows": "write", "metadata": "read",
	}}}, nil
}
func (discoveryTokenSource) ListRepositories(context.Context, string) ([]githubapp.Repository, error) {
	return []githubapp.Repository{{NodeID: "R_1", FullName: "pingcap/repo", Private: true, Visibility: "private"}}, nil
}

func TestFactoryListsSafeGitHubPickerData(t *testing.T) {
	factory := &Factory{Tokens: discoveryTokenSource{}}
	installations, err := factory.ListGitHubInstallations(context.Background())
	if err != nil || len(installations) != 1 {
		t.Fatalf("installations=%#v err=%v", installations, err)
	}
	if installations[0].ConnectionID != "github-app-installation:9" || !installations[0].PermissionsValid {
		t.Fatalf("installation=%#v", installations[0])
	}
	if installations[0].ManagementURL != "https://github.com/settings/installations/9" {
		t.Fatalf("management URL=%q", installations[0].ManagementURL)
	}
	repositories, err := factory.ListGitHubRepositories(context.Background(), installations[0].ConnectionID)
	if err != nil || len(repositories) != 1 || repositories[0].ID != "R_1" || repositories[0].FullName != "pingcap/repo" {
		t.Fatalf("repositories=%#v err=%v", repositories, err)
	}
}

type permissionedTokenSource struct{}

func (permissionedTokenSource) APIURL() string { return "https://api.github.test" }
func (permissionedTokenSource) Token(context.Context, string) (githubapp.InstallationToken, error) {
	return githubapp.InstallationToken{Value: "token", ExpiresAt: time.Now().Add(time.Hour), Permissions: map[string]string{
		"contents": "write", "issues": "write", "pull_requests": "write", "workflows": "write", "metadata": "read",
	}}, nil
}

func TestResolveRepositoryUsesImmutableNodeIDWhenLocatorIsOmitted(t *testing.T) {
	factory := &Factory{Tokens: permissionedTokenSource{}, HTTPClient: &http.Client{Transport: roundTripFunc(func(request *http.Request) (*http.Response, error) {
		body := `{"node_id":"R_node","full_name":"octo/repo"}`
		if request.Method == http.MethodPost {
			body = `{"data":{"node":{"id":"R_node","nameWithOwner":"octo/repo"}}}`
		}
		return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(body)), Header: make(http.Header)}, nil
	})}}
	fullName, err := factory.ResolveRepository(context.Background(), "github-app-installation:1", "R_node", "")
	if err != nil {
		t.Fatal(err)
	}
	if fullName != "octo/repo" {
		t.Fatalf("full name=%q", fullName)
	}
}

func TestResolveRepositoryReturnsValidatedVisibility(t *testing.T) {
	factory := &Factory{Tokens: permissionedTokenSource{}, HTTPClient: &http.Client{Transport: roundTripFunc(func(request *http.Request) (*http.Response, error) {
		return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(`{"node_id":"R_private","full_name":"octo/private","visibility":"private","private":true}`)), Header: make(http.Header)}, nil
	})}}
	fullName, visibility, err := factory.ResolveRepositoryWithVisibility(context.Background(), "github-app-installation:1", "R_private", "octo/private")
	if err != nil {
		t.Fatal(err)
	}
	if fullName != "octo/private" || visibility != "private" {
		t.Fatalf("full name=%q visibility=%q", fullName, visibility)
	}
}

func TestIsEmptyIgnoresDefaultLabelsButDetectsBusinessMetadata(t *testing.T) {
	fakeBin := t.TempDir()
	fakeGit := filepath.Join(fakeBin, "git")
	if err := os.WriteFile(fakeGit, []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil {
		t.Fatal(err)
	}
	t.Setenv("PATH", fakeBin+string(os.PathListSeparator)+os.Getenv("PATH"))

	for _, test := range []struct {
		name      string
		issueBody string
		wantEmpty bool
	}{
		{name: "provider default labels only", issueBody: "[]", wantEmpty: true},
		{name: "issue exists", issueBody: `[{"id":1}]`, wantEmpty: false},
	} {
		t.Run(test.name, func(t *testing.T) {
			labelsRequested := false
			httpClient := &http.Client{Transport: roundTripFunc(func(request *http.Request) (*http.Response, error) {
				if strings.HasSuffix(request.URL.Path, "/labels") {
					labelsRequested = true
				}
				body := "[]"
				if strings.HasSuffix(request.URL.Path, "/issues") {
					body = test.issueBody
				}
				return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(body)), Header: make(http.Header)}, nil
			})}
			client, err := newAPIClient(permissionedTokenSource{}, "github-app-installation:1", httpClient)
			if err != nil {
				t.Fatal(err)
			}
			provider := &Provider{
				endpoint: reposync.Endpoint{Provider: "github", RepositoryFullName: "octo/repo", ConnectionID: "github-app-installation:1"},
				client:   client,
			}
			empty, err := provider.IsEmpty(context.Background())
			if err != nil {
				t.Fatal(err)
			}
			if empty != test.wantEmpty {
				t.Fatalf("empty=%t, want %t", empty, test.wantEmpty)
			}
			if labelsRequested {
				t.Fatal("GitHub default labels were treated as business data during empty-target detection")
			}
		})
	}
}

func TestProjectedPullRequestCarrierReconstructsCanonicalState(t *testing.T) {
	source := canonical.NewRecord(canonical.TypePullRequest, "ags-pr-1", "pull-request:ags-pr-1", map[string]any{
		"title": "merged change", "body": "details", "state": "closed", "draft": false,
		"head_ref": "feature", "head_sha": "abc", "base_ref": "main", "base_sha": "def",
		"merged": true, "merge_commit_sha": "deadbeef", "labels": []string{"bug"},
		"assignees": []string{"source-user"}, "milestone": "v1",
	})
	metadata, err := source.Payload()
	if err != nil {
		t.Fatal(err)
	}
	managedBody := canonical.RenderManagedBodyWithNotice("details", "https://ags.test/pull/1", "alice", nil, 42, "projected")
	value := map[string]any{
		"id": float64(99), "node_id": "I_carrier", "number": float64(7), "title": "merged change",
		"body": managedBody, "state": "closed", "labels": []any{map[string]any{"name": "bug"}},
		"assignees": []any{}, "milestone": map[string]any{"title": "v1"},
	}
	parsed := canonical.StripManagedBody(managedBody)
	carrier, err := projectedPullRequestCarrier(json.RawMessage(metadata), value, parsed)
	if err != nil {
		t.Fatal(err)
	}
	if carrier.Identity != "I_carrier" || canonical.StringField(carrier.Fields, "_carrier") != "issue" || carrier.ManagedLinkID != 42 {
		t.Fatalf("carrier=%#v", carrier)
	}
	sourceHash, err := source.ComparableHash()
	if err != nil {
		t.Fatal(err)
	}
	carrierHash, err := carrier.ComparableHash()
	if err != nil {
		t.Fatal(err)
	}
	if carrierHash != sourceHash {
		t.Fatalf("carrier hash=%s, source hash=%s", carrierHash, sourceHash)
	}
}

func TestPullRequestMergeCommitSHASkipsGitHubSyntheticTestMerge(t *testing.T) {
	open := map[string]any{"merged_at": nil, "merge_commit_sha": "synthetic-test-merge"}
	if got := pullRequestMergeCommitSHA(open); got != "" {
		t.Fatalf("open pull request merge commit SHA = %q, want empty", got)
	}
	merged := map[string]any{"merged_at": "2026-08-21T00:00:00Z", "merge_commit_sha": "real-merge"}
	if got := pullRequestMergeCommitSHA(merged); got != "real-merge" {
		t.Fatalf("merged pull request merge commit SHA = %q", got)
	}
}

func TestProjectedConversationCarrierKeepsLogicalReviewType(t *testing.T) {
	source := canonical.NewRecord(canonical.TypePullRequestReviewComment, "ags-comment", "pull-review-comment:ags-comment", map[string]any{"body": "nit", "path": "a.go", "line": 4})
	metadata, err := source.Payload()
	if err != nil {
		t.Fatal(err)
	}
	body := canonical.RenderManagedBodyWithNotice("nit", "", "", nil, 9, "projected")
	carrier, err := projectedConversationCarrier(managedLinkExpectation{objectType: canonical.TypePullRequestReviewComment, metadata: metadata}, map[string]any{
		"id": float64(10), "node_id": "IC_carrier", "body": body,
	}, canonical.StripManagedBody(body), 3, "PR_carrier")
	if err != nil {
		t.Fatal(err)
	}
	if carrier.Type != canonical.TypePullRequestReviewComment || carrier.ParentIdentity != "PR_carrier" || canonical.StringField(carrier.Fields, "_carrier") != "issue_comment" {
		t.Fatalf("carrier=%#v", carrier)
	}
}

func TestGitRemoteDerivesGitHubDotComHostFromAPIURL(t *testing.T) {
	client, err := newAPIClient(githubDotComTokenSource{}, "github-app-installation:1", nil)
	if err != nil {
		t.Fatal(err)
	}
	provider := &Provider{
		endpoint: reposync.Endpoint{Provider: "github", RepositoryFullName: "octo/repo", ConnectionID: "github-app-installation:1"},
		client:   client,
	}
	remote, err := provider.GitRemote(context.Background(), false)
	if err != nil {
		t.Fatal(err)
	}
	if remote.URL != "https://github.com/octo/repo.git" {
		t.Fatalf("Git remote URL = %q", remote.URL)
	}
}

func TestGitRemoteDerivesEnterpriseHostFromAPIURL(t *testing.T) {
	tokens := enterpriseTokenSource{}
	client, err := newAPIClient(tokens, "github-app-installation:1", nil)
	if err != nil {
		t.Fatal(err)
	}
	provider := &Provider{endpoint: reposync.Endpoint{Provider: "github", RepositoryFullName: "octo/repo", ConnectionID: "github-app-installation:1"}, client: client}
	remote, err := provider.GitRemote(context.Background(), true)
	if err != nil {
		t.Fatal(err)
	}
	if remote.URL != "https://github.example.com/octo/repo.wiki.git" {
		t.Fatalf("Git remote URL = %q", remote.URL)
	}
}

type enterpriseTokenSource struct{ staticTokenSource }

func (enterpriseTokenSource) APIURL() string { return "https://github.example.com/api/v3" }

type githubDotComTokenSource struct{ staticTokenSource }

func (githubDotComTokenSource) APIURL() string { return "https://api.github.com" }

func TestManagedBodyRequiresLinkAndTargetIdentityMatch(t *testing.T) {
	provider := &Provider{}
	provider.SetObjectLinks([]reposync.LinkView{{ID: 42, ObjectType: canonical.TypeIssue, SourceIdentity: "source", TargetIdentity: "target"}}, false)
	body := canonical.RenderManagedBody("user body", "https://ags.test/o/r/issues/1", "alice", nil, 42)
	if parsed := provider.trustedManagedBody(body, canonical.TypeIssue, "target"); parsed.Body != "user body" || parsed.ObjectLinkID != 42 {
		t.Fatalf("trusted marker was not stripped: %#v", parsed)
	}
	if parsed := provider.trustedManagedBody(body, canonical.TypeIssue, "different-object"); parsed.Body != body || parsed.ObjectLinkID != 0 {
		t.Fatalf("marker on wrong identity was trusted: %#v", parsed)
	}
	if parsed := provider.trustedManagedBody(body, canonical.TypePullRequest, "target"); parsed.Body != body || parsed.ObjectLinkID != 0 {
		t.Fatalf("marker on wrong object type was trusted: %#v", parsed)
	}
	provider.SetObjectLinks(nil, false)
	if parsed := provider.trustedManagedBody(body, canonical.TypeIssue, "target"); parsed.Body != body || parsed.ObjectLinkID != 0 {
		t.Fatalf("unlinked marker was trusted: %#v", parsed)
	}
}

func TestManagedBodyAllowsPendingCreateLinkDuringReadBack(t *testing.T) {
	provider := &Provider{}
	linkID := uint(99)
	provider.trustBatchObjectLinks([]reposync.PlannedItem{{ObjectLinkID: &linkID, ObjectType: canonical.TypeIssue}})
	body := canonical.RenderManagedBody("created", "", "", nil, linkID)
	if parsed := provider.trustedManagedBody(body, canonical.TypeIssue, "new-provider-id"); parsed.Body != "created" || parsed.ObjectLinkID != linkID {
		t.Fatalf("pending create marker was not trusted: %#v", parsed)
	}
}

func TestRecoverCreateRetriesEventuallyVisibleMarker(t *testing.T) {
	linkID := uint(6)
	source := canonical.NewRecord(canonical.TypeIssue, "1", "issue:1", map[string]any{
		"title": "Full bootstrap issue", "body": "Issue created on AGS before the first full sync.",
		"state": "open", "state_reason": "", "locked": false, "labels": []string{"sync-e2e"},
		"assignees": []string{}, "milestone": "Sync E2E milestone",
	})
	payload, err := source.Payload()
	if err != nil {
		t.Fatal(err)
	}
	desiredHash, err := source.ComparableHash()
	if err != nil {
		t.Fatal(err)
	}
	issueBody := canonical.RenderManagedBody(source.Fields["body"].(string), "", "", nil, linkID)
	issueJSON, err := json.Marshal([]map[string]any{{
		"id": 10, "node_id": "I_10", "number": 1, "title": source.Fields["title"], "body": issueBody,
		"state": "open", "state_reason": nil, "locked": false,
		"labels": []map[string]any{{"name": "sync-e2e"}}, "assignees": []any{},
		"milestone": map[string]any{"title": "Sync E2E milestone"},
	}})
	if err != nil {
		t.Fatal(err)
	}
	listCalls := 0
	httpClient := &http.Client{Transport: roundTripFunc(func(request *http.Request) (*http.Response, error) {
		listCalls++
		body := "[]"
		if listCalls > 1 {
			body = string(issueJSON)
		}
		return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(body)), Header: make(http.Header)}, nil
	})}
	client, err := newAPIClient(permissionedTokenSource{}, "github-app-installation:1", httpClient)
	if err != nil {
		t.Fatal(err)
	}
	provider := &Provider{
		endpoint: reposync.Endpoint{Provider: "github", RepositoryFullName: "octo/repo", ConnectionID: "github-app-installation:1"},
		client:   client, inventoryByIdentity: make(map[string]reposync.InventoryObject),
		issueIdentityByNumber: make(map[int]string), pullIdentityByNumber: make(map[int]string),
	}
	provider.SetObjectLinks([]reposync.LinkView{{ID: linkID, ObjectType: canonical.TypeIssue}}, false)
	state, err := provider.Recover(context.Background(), reposync.PlannedItem{
		ObjectLinkID: &linkID, ObjectType: canonical.TypeIssue, ObjectKey: "issue:1",
		Action: reposync.ActionCreate, DesiredHash: desiredHash, Payload: payload,
	})
	if err != nil {
		t.Fatal(err)
	}
	if !state.Exists || state.Identity != "I_10" || state.Hash != desiredHash || listCalls != 2 {
		t.Fatalf("state=%#v listCalls=%d", state, listCalls)
	}
}

func TestValidateSyncPermissions(t *testing.T) {
	valid := map[string]string{"contents": "write", "issues": "write", "pull_requests": "write", "workflows": "write", "metadata": "read"}
	if err := validateSyncPermissions(valid); err != nil {
		t.Fatal(err)
	}
	delete(valid, "workflows")
	if err := validateSyncPermissions(valid); err == nil {
		t.Fatal("missing workflows permission was accepted")
	}
}

func TestCreateMilestoneOmitsEmptyDueOn(t *testing.T) {
	var requestBody map[string]any
	httpClient := &http.Client{Transport: roundTripFunc(func(request *http.Request) (*http.Response, error) {
		if err := json.NewDecoder(request.Body).Decode(&requestBody); err != nil {
			t.Fatal(err)
		}
		body := `{"node_id":"MI_1","number":1,"title":"v1"}`
		return &http.Response{StatusCode: http.StatusCreated, Body: io.NopCloser(strings.NewReader(body)), Header: make(http.Header)}, nil
	})}
	client, err := newAPIClient(permissionedTokenSource{}, "github-app-installation:1", httpClient)
	if err != nil {
		t.Fatal(err)
	}
	provider := &Provider{
		endpoint: reposync.Endpoint{Provider: "github", RepositoryFullName: "octo/repo", ConnectionID: "github-app-installation:1"},
		client:   client,
	}
	record := canonical.NewRecord(canonical.TypeMilestone, "1", "milestone:1", map[string]any{
		"title": "v1", "description": "", "state": "open", "due_on": "",
	})
	if _, err := provider.applyMilestone(context.Background(), reposync.PlannedItem{Action: reposync.ActionCreate}, record); err != nil {
		t.Fatal(err)
	}
	if _, ok := requestBody["due_on"]; ok {
		t.Fatalf("empty due_on must be omitted, request=%#v", requestBody)
	}
}

func TestVerifyTreatsClosedIssueAsProjectedDeletion(t *testing.T) {
	record := canonical.NewRecord(canonical.TypeIssue, "10", "issue:10", map[string]any{"state": "closed"})
	object, err := record.Inventory("revision", "projected")
	if err != nil {
		t.Fatal(err)
	}
	provider := &Provider{inventoryByIdentity: map[string]reposync.InventoryObject{identityKey(object.ObjectType, object.Identity): object}}
	state, err := provider.Verify(context.Background(), reposync.PlannedItem{ObjectType: canonical.TypeIssue, Action: reposync.ActionDelete, TargetIdentity: "10"}, reposync.ApplyResult{TargetIdentity: "10"})
	if err != nil {
		t.Fatal(err)
	}
	if state.Exists {
		t.Fatal("closed GitHub Issue did not satisfy projected deletion")
	}
}
