package status

import (
	"bytes"
	"io"
	"net/http"
	"regexp"
	"strings"
	"testing"

	"github.com/cli/cli/v2/context"
	"github.com/cli/cli/v2/git"
	"github.com/cli/cli/v2/internal/config"
	fd "github.com/cli/cli/v2/internal/featuredetection"
	"github.com/cli/cli/v2/internal/gh"
	"github.com/cli/cli/v2/internal/ghrepo"
	"github.com/cli/cli/v2/internal/run"
	"github.com/cli/cli/v2/pkg/cmdutil"
	"github.com/cli/cli/v2/pkg/httpmock"
	"github.com/cli/cli/v2/pkg/iostreams"
	"github.com/cli/cli/v2/test"
	"github.com/google/shlex"
	"github.com/stretchr/testify/assert"
)

func runCommand(rt http.RoundTripper, branch string, isTTY bool, cli string) (*test.CmdOut, error) {
	return runCommandWithDetector(rt, branch, isTTY, cli, &fd.DisabledDetectorMock{})
}

func runCommandWithDetector(rt http.RoundTripper, branch string, isTTY bool, cli string, detector fd.Detector) (*test.CmdOut, error) {
	ios, _, stdout, stderr := iostreams.Test()
	ios.SetStdoutTTY(isTTY)
	ios.SetStdinTTY(isTTY)
	ios.SetStderrTTY(isTTY)

	factory := &cmdutil.Factory{
		IOStreams: ios,
		HttpClient: func() (*http.Client, error) {
			return &http.Client{Transport: rt}, nil
		},
		Config: func() (gh.Config, error) {
			return config.NewBlankConfig(), nil
		},
		BaseRepo: func() (ghrepo.Interface, error) {
			return ghrepo.New("OWNER", "REPO"), nil
		},
		Remotes: func() (context.Remotes, error) {
			return context.Remotes{
				{
					Remote: &git.Remote{Name: "origin"},
					Repo:   ghrepo.New("OWNER", "REPO"),
				},
			}, nil
		},
		Branch: func() (string, error) {
			if branch == "" {
				return "", git.ErrNotOnAnyBranch
			}
			return branch, nil
		},
		GitClient: &git.Client{GitPath: "some/path/git"},
	}

	withProvidedDetector := func(opts *StatusOptions) error {
		opts.Detector = detector
		return statusRun(opts)
	}

	cmd := NewCmdStatus(factory, withProvidedDetector)
	cmd.PersistentFlags().StringP("repo", "R", "", "")

	argv, err := shlex.Split(cli)
	if err != nil {
		return nil, err
	}
	cmd.SetArgs(argv)

	cmd.SetIn(&bytes.Buffer{})
	cmd.SetOut(io.Discard)
	cmd.SetErr(io.Discard)

	_, err = cmd.ExecuteC()
	return &test.CmdOut{
		OutBuf: stdout,
		ErrBuf: stderr,
	}, err
}

func initFakeHTTP() *httpmock.Registry {
	return &httpmock.Registry{}
}

func TestPRStatus(t *testing.T) {
	http := initFakeHTTP()
	defer http.Verify(t)
	http.Register(httpmock.GraphQL(`query PullRequestStatus\b`), httpmock.FileResponse("./fixtures/prStatus.json"))

	// stub successful git commands
	rs, cleanup := run.Stub()
	defer cleanup(t)
	rs.Register(`git rev-parse --symbolic-full-name blueberries@{push}`, 128, "")
	rs.Register(`git config --get-regexp \^branch\\.`, 0, "")
	rs.Register(`git config remote.pushDefault`, 0, "")
	rs.Register(`git config push.default`, 1, "")

	output, err := runCommand(http, "blueberries", true, "")
	if err != nil {
		t.Errorf("error running command `pr status`: %v", err)
	}

	expectedPrs := []*regexp.Regexp{
		regexp.MustCompile(`#8.*\[strawberries\]`),
		regexp.MustCompile(`#9.*\[apples\].*✓ Auto-merge enabled`),
		regexp.MustCompile(`#10.*\[blueberries\]`),
		regexp.MustCompile(`#11.*\[figs\]`),
	}

	for _, r := range expectedPrs {
		if !r.MatchString(output.String()) {
			t.Errorf("output did not match regexp /%s/", r)
		}
	}
}

func TestPRStatus_reviewsAndChecks(t *testing.T) {
	http := initFakeHTTP()
	defer http.Verify(t)
	// status,conclusion matches the old StatusContextRollup query
	http.Register(httpmock.GraphQL(`status,conclusion`), httpmock.FileResponse("./fixtures/prStatusChecks.json"))

	// stub successful git command
	rs, cleanup := run.Stub()
	defer cleanup(t)
	rs.Register(`git config --get-regexp \^branch\\.`, 0, "")
	rs.Register(`git config remote.pushDefault`, 0, "")
	rs.Register(`git rev-parse --symbolic-full-name blueberries@{push}`, 128, "")
	rs.Register(`git config push.default`, 1, "")

	output, err := runCommand(http, "blueberries", true, "")
	if err != nil {
		t.Errorf("error running command `pr status`: %v", err)
	}

	expected := []string{
		"✓ Checks passing + Changes requested ! Merge conflict status unknown",
		"- Checks pending ✓ 2 Approved",
		"× 1/3 checks failing - Review required ✓ No merge conflicts",
		"✓ Checks passing × Merge conflicts",
	}

	for _, line := range expected {
		if !strings.Contains(output.String(), line) {
			t.Errorf("output did not contain %q: %q", line, output.String())
		}
	}
}

func TestPRStatus_reviewsAndChecksWithStatesByCount(t *testing.T) {
	http := initFakeHTTP()
	defer http.Verify(t)
	// checkRunCount,checkRunCountsByState matches the new StatusContextRollup query
	http.Register(httpmock.GraphQL(`checkRunCount,checkRunCountsByState`), httpmock.FileResponse("./fixtures/prStatusChecksWithStatesByCount.json"))

	// stub successful git command
	rs, cleanup := run.Stub()
	defer cleanup(t)
	rs.Register(`git config --get-regexp \^branch\\.`, 0, "")
	rs.Register(`git config remote.pushDefault`, 0, "")
	rs.Register(`git rev-parse --symbolic-full-name blueberries@{push}`, 128, "")
	rs.Register(`git config push.default`, 1, "")

	output, err := runCommandWithDetector(http, "blueberries", true, "", &fd.EnabledDetectorMock{})
	if err != nil {
		t.Errorf("error running command `pr status`: %v", err)
	}

	expected := []string{
		"✓ Checks passing + Changes requested ! Merge conflict status unknown",
		"- Checks pending ✓ 2 Approved",
		"× 1/3 checks failing - Review required ✓ No merge conflicts",
		"✓ Checks passing × Merge conflicts",
	}

	for _, line := range expected {
		if !strings.Contains(output.String(), line) {
			t.Errorf("output did not contain %q: %q", line, output.String())
		}
	}
}

func TestPRStatus_currentBranch_showTheMostRecentPR(t *testing.T) {
	http := initFakeHTTP()
	defer http.Verify(t)
	http.Register(httpmock.GraphQL(`query PullRequestStatus\b`), httpmock.FileResponse("./fixtures/prStatusCurrentBranch.json"))

	// stub successful git command
	rs, cleanup := run.Stub()
	defer cleanup(t)
	rs.Register(`git config --get-regexp \^branch\\.`, 0, "")
	rs.Register(`git config remote.pushDefault`, 0, "")
	rs.Register(`git rev-parse --symbolic-full-name blueberries@{push}`, 128, "")
	rs.Register(`git config push.default`, 1, "")

	output, err := runCommand(http, "blueberries", true, "")
	if err != nil {
		t.Errorf("error running command `pr status`: %v", err)
	}

	expectedLine := regexp.MustCompile(`#10  Blueberries are certainly a good fruit \[blueberries\]`)
	if !expectedLine.MatchString(output.String()) {
		t.Errorf("output did not match regexp /%s/\n> output\n%s\n", expectedLine, output)
		return
	}

	unexpectedLines := []*regexp.Regexp{
		regexp.MustCompile(`#9  Blueberries are a good fruit \[blueberries\] - Merged`),
		regexp.MustCompile(`#8  Blueberries are probably a good fruit \[blueberries\] - Closed`),
	}
	for _, r := range unexpectedLines {
		if r.MatchString(output.String()) {
			t.Errorf("output unexpectedly match regexp /%s/\n> output\n%s\n", r, output)
			return
		}
	}
}

func TestPRStatus_currentBranch_defaultBranch(t *testing.T) {
	http := initFakeHTTP()
	defer http.Verify(t)
	http.Register(httpmock.GraphQL(`query PullRequestStatus\b`), httpmock.FileResponse("./fixtures/prStatusCurrentBranch.json"))

	// stub successful git command
	rs, cleanup := run.Stub()
	defer cleanup(t)
	rs.Register(`git config --get-regexp \^branch\\.`, 0, "")
	rs.Register(`git config remote.pushDefault`, 0, "")
	rs.Register(`git rev-parse --symbolic-full-name blueberries@{push}`, 128, "")
	rs.Register(`git config push.default`, 1, "")

	output, err := runCommand(http, "blueberries", true, "")
	if err != nil {
		t.Errorf("error running command `pr status`: %v", err)
	}

	expectedLine := regexp.MustCompile(`#10  Blueberries are certainly a good fruit \[blueberries\]`)
	if !expectedLine.MatchString(output.String()) {
		t.Errorf("output did not match regexp /%s/\n> output\n%s\n", expectedLine, output)
		return
	}
}

func TestPRStatus_currentBranch_defaultBranch_repoFlag(t *testing.T) {
	http := initFakeHTTP()
	defer http.Verify(t)
	http.Register(httpmock.GraphQL(`query PullRequestStatus\b`), httpmock.FileResponse("./fixtures/prStatusCurrentBranchClosedOnDefaultBranch.json"))

	output, err := runCommand(http, "blueberries", true, "-R OWNER/REPO")
	if err != nil {
		t.Errorf("error running command `pr status`: %v", err)
	}

	expectedLine := regexp.MustCompile(`#8  Blueberries are a good fruit \[blueberries\]`)
	if expectedLine.MatchString(output.String()) {
		t.Errorf("output not expected to match regexp /%s/\n> output\n%s\n", expectedLine, output)
		return
	}
}

func TestPRStatus_currentBranch_Closed(t *testing.T) {
	http := initFakeHTTP()
	defer http.Verify(t)
	http.Register(httpmock.GraphQL(`query PullRequestStatus\b`), httpmock.FileResponse("./fixtures/prStatusCurrentBranchClosed.json"))

	// stub successful git command
	rs, cleanup := run.Stub()
	defer cleanup(t)
	rs.Register(`git config --get-regexp \^branch\\.`, 0, "")
	rs.Register(`git config remote.pushDefault`, 0, "")
	rs.Register(`git rev-parse --symbolic-full-name blueberries@{push}`, 128, "")
	rs.Register(`git config push.default`, 1, "")

	output, err := runCommand(http, "blueberries", true, "")
	if err != nil {
		t.Errorf("error running command `pr status`: %v", err)
	}

	expectedLine := regexp.MustCompile(`#8  Blueberries are a good fruit \[blueberries\] - Closed`)
	if !expectedLine.MatchString(output.String()) {
		t.Errorf("output did not match regexp /%s/\n> output\n%s\n", expectedLine, output)
		return
	}
}

func TestPRStatus_currentBranch_Closed_defaultBranch(t *testing.T) {
	http := initFakeHTTP()
	defer http.Verify(t)
	http.Register(httpmock.GraphQL(`query PullRequestStatus\b`), httpmock.FileResponse("./fixtures/prStatusCurrentBranchClosedOnDefaultBranch.json"))

	// stub successful git command
	rs, cleanup := run.Stub()
	defer cleanup(t)
	rs.Register(`git config --get-regexp \^branch\\.`, 0, "")
	rs.Register(`git config remote.pushDefault`, 0, "")
	rs.Register(`git rev-parse --symbolic-full-name blueberries@{push}`, 128, "")
	rs.Register(`git config push.default`, 1, "")

	output, err := runCommand(http, "blueberries", true, "")
	if err != nil {
		t.Errorf("error running command `pr status`: %v", err)
	}

	expectedLine := regexp.MustCompile(`There is no pull request associated with \[blueberries\]`)
	if !expectedLine.MatchString(output.String()) {
		t.Errorf("output did not match regexp /%s/\n> output\n%s\n", expectedLine, output)
		return
	}
}

func TestPRStatus_currentBranch_Merged(t *testing.T) {
	http := initFakeHTTP()
	defer http.Verify(t)
	http.Register(httpmock.GraphQL(`query PullRequestStatus\b`), httpmock.FileResponse("./fixtures/prStatusCurrentBranchMerged.json"))

	// stub successful git command
	rs, cleanup := run.Stub()
	defer cleanup(t)
	rs.Register(`git config --get-regexp \^branch\\.`, 0, "")
	rs.Register(`git config remote.pushDefault`, 0, "")
	rs.Register(`git rev-parse --symbolic-full-name blueberries@{push}`, 128, "")
	rs.Register(`git config push.default`, 1, "")

	output, err := runCommand(http, "blueberries", true, "")
	if err != nil {
		t.Errorf("error running command `pr status`: %v", err)
	}

	expectedLine := regexp.MustCompile(`#8  Blueberries are a good fruit \[blueberries\] - Merged`)
	if !expectedLine.MatchString(output.String()) {
		t.Errorf("output did not match regexp /%s/\n> output\n%s\n", expectedLine, output)
		return
	}
}

func TestPRStatus_currentBranch_Merged_defaultBranch(t *testing.T) {
	http := initFakeHTTP()
	defer http.Verify(t)
	http.Register(httpmock.GraphQL(`query PullRequestStatus\b`), httpmock.FileResponse("./fixtures/prStatusCurrentBranchMergedOnDefaultBranch.json"))

	// stub successful git command
	rs, cleanup := run.Stub()
	defer cleanup(t)
	rs.Register(`git config --get-regexp \^branch\\.`, 0, "")
	rs.Register(`git config remote.pushDefault`, 0, "")
	rs.Register(`git rev-parse --symbolic-full-name blueberries@{push}`, 128, "")
	rs.Register(`git config push.default`, 1, "")

	output, err := runCommand(http, "blueberries", true, "")
	if err != nil {
		t.Errorf("error running command `pr status`: %v", err)
	}

	expectedLine := regexp.MustCompile(`There is no pull request associated with \[blueberries\]`)
	if !expectedLine.MatchString(output.String()) {
		t.Errorf("output did not match regexp /%s/\n> output\n%s\n", expectedLine, output)
		return
	}
}

func TestPRStatus_blankSlate(t *testing.T) {
	http := initFakeHTTP()
	defer http.Verify(t)
	http.Register(httpmock.GraphQL(`query PullRequestStatus\b`), httpmock.StringResponse(`{"data": {}}`))

	// stub successful git command
	rs, cleanup := run.Stub()
	defer cleanup(t)
	rs.Register(`git config --get-regexp \^branch\\.`, 0, "")
	rs.Register(`git config remote.pushDefault`, 0, "")
	rs.Register(`git rev-parse --symbolic-full-name blueberries@{push}`, 128, "")
	rs.Register(`git config push.default`, 1, "")

	output, err := runCommand(http, "blueberries", true, "")
	if err != nil {
		t.Errorf("error running command `pr status`: %v", err)
	}

	expected := `
Relevant pull requests in OWNER/REPO

Current branch
  There is no pull request associated with [blueberries]

Created by you
  You have no open pull requests

Requesting a code review from you
  You have no pull requests to review

`
	if output.String() != expected {
		t.Errorf("expected %q, got %q", expected, output.String())
	}
}

func TestPRStatus_blankSlateRepoOverride(t *testing.T) {
	http := initFakeHTTP()
	defer http.Verify(t)
	http.Register(httpmock.GraphQL(`query PullRequestStatus\b`), httpmock.StringResponse(`{"data": {}}`))

	output, err := runCommand(http, "blueberries", true, "--repo OWNER/REPO")
	if err != nil {
		t.Errorf("error running command `pr status`: %v", err)
	}

	expected := `
Relevant pull requests in OWNER/REPO

Created by you
  You have no open pull requests

Requesting a code review from you
  You have no pull requests to review

`
	if output.String() != expected {
		t.Errorf("expected %q, got %q", expected, output.String())
	}
}

func TestPRStatus_detachedHead(t *testing.T) {
	http := initFakeHTTP()
	defer http.Verify(t)
	http.Register(httpmock.GraphQL(`query PullRequestStatus\b`), httpmock.StringResponse(`{"data": {}}`))

	output, err := runCommand(http, "", true, "")
	if err != nil {
		t.Errorf("error running command `pr status`: %v", err)
	}

	expected := `
Relevant pull requests in OWNER/REPO

Current branch
  There is no current branch

Created by you
  You have no open pull requests

Requesting a code review from you
  You have no pull requests to review

`
	if output.String() != expected {
		t.Errorf("expected %q, got %q", expected, output.String())
	}
}

func TestPRStatus_error_ReadBranchConfig(t *testing.T) {
	rs, cleanup := run.Stub()
	defer cleanup(t)
	// We only need the one stub because this fails early
	rs.Register(`git config --get-regexp \^branch\\.`, 2, "")
	_, err := runCommand(initFakeHTTP(), "blueberries", true, "")
	assert.Error(t, err)
}
