package sharedrunners

import (
	"io"
	"net/http"
	"net/url"
	"strings"
	"testing"

	"gitcode.com/gitcode-cli/cli/api"
	cmdutil "gitcode.com/gitcode-cli/cli/pkg/cmdutil"
	"gitcode.com/gitcode-cli/cli/pkg/iostreams"
	"gitcode.com/gitcode-cli/cli/pkg/testutil"
)

func TestNewCmdList(t *testing.T) {
	tests := []struct {
		name    string
		args    []string
		wantErr bool
	}{
		{name: "list with repo", args: []string{"-R", "owner/repo"}, wantErr: false},
		{name: "list with keyword", args: []string{"-R", "owner/repo", "--keyword", "prod"}, wantErr: false},
		{name: "list with json", args: []string{"-R", "owner/repo", "--json"}, wantErr: false},
		{name: "list with paginate", args: []string{"-R", "owner/repo", "--paginate"}, wantErr: false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			f := cmdutil.TestFactory()
			cmd := NewCmdList(f, func(opts *ListOptions) error {
				return nil
			})
			cmd.SetArgs(tt.args)

			err := cmd.Execute()
			if (err != nil) != tt.wantErr {
				t.Errorf("Execute() error = %v, wantErr %v", err, tt.wantErr)
			}
		})
	}
}

func TestListRunBuildsV8Query(t *testing.T) {
	t.Setenv("GC_TOKEN", "test-token")

	f := cmdutil.TestFactory()
	var gotPath string
	opts := &ListOptions{
		IO: f.IOStreams,
		HttpClient: func() (*http.Client, error) {
			return &http.Client{
				Transport: testutil.NewRoundTripFunc(func(req *http.Request) (*http.Response, error) {
					gotPath = req.URL.Path
					if req.URL.RawQuery != "" {
						gotPath += "?" + req.URL.RawQuery
					}
					return listTestResponse(http.StatusOK, `{"total_count":0,"runners":[]}`), nil
				}),
			}, nil
		},
		Repository: "owner/repo",
		Keyword:    "prod",
		Limit:      25,
		Page:       2,
	}

	if err := listRun(opts); err != nil {
		t.Fatalf("listRun() error = %v", err)
	}

	want := "/api/v8/repos/owner/repo/actions/runners/shared-runners"
	if !strings.HasPrefix(gotPath, want+"?") {
		t.Fatalf("request path = %q, want v8 shared-runners prefix", gotPath)
	}
	assertNoAccessTokenQuery(t, gotPath)

	parsed, err := url.Parse(gotPath)
	if err != nil {
		t.Fatalf("url.Parse() error = %v", err)
	}
	q := parsed.Query()
	for _, key := range []string{"keyword", "per_page", "page"} {
		if _, ok := q[key]; !ok {
			t.Fatalf("query missing %s in %s", key, q.Encode())
		}
	}
	if q.Get("keyword") != "prod" {
		t.Fatalf("keyword = %q, want prod", q.Get("keyword"))
	}
}

func TestListRunJSONOutput(t *testing.T) {
	t.Setenv("GC_TOKEN", "test-token")

	io, _, out, _ := iostreams.Test()
	opts := &ListOptions{
		IO: io,
		HttpClient: func() (*http.Client, error) {
			return &http.Client{
				Transport: testutil.NewRoundTripFunc(func(req *http.Request) (*http.Response, error) {
					return listTestResponse(http.StatusOK, `{"total_count":1,"runners":[{"id":"r1","runner_group_id":"rg1","runner_name":"shared-1","name":"host","work_dir":"/tmp","labels":[{"label_name":"self-hosted","label_value":"","label_color":""}]}]}`), nil
				}),
			}, nil
		},
		Repository: "owner/repo",
		Limit:      30,
		JSON:       true,
	}

	if err := listRun(opts); err != nil {
		t.Fatalf("listRun() error = %v", err)
	}

	got := strings.TrimSpace(out.String())
	if !strings.Contains(got, `"id": "r1"`) {
		t.Fatalf("JSON output = %q, missing id", got)
	}
	if !strings.Contains(got, `"runner_name": "shared-1"`) {
		t.Fatalf("JSON output = %q, missing runner_name", got)
	}
	if !strings.Contains(got, `"label_name": "self-hosted"`) {
		t.Fatalf("JSON output = %q, missing label_name", got)
	}
}

func TestListRunEmptyResultsJSON(t *testing.T) {
	t.Setenv("GC_TOKEN", "test-token")

	io, _, out, _ := iostreams.Test()
	opts := &ListOptions{
		IO: io,
		HttpClient: func() (*http.Client, error) {
			return &http.Client{
				Transport: testutil.NewRoundTripFunc(func(req *http.Request) (*http.Response, error) {
					return listTestResponse(http.StatusOK, `{"total_count":0,"runners":[]}`), nil
				}),
			}, nil
		},
		Repository: "owner/repo",
		Limit:      30,
		JSON:       true,
	}

	if err := listRun(opts); err != nil {
		t.Fatalf("listRun() error = %v", err)
	}

	got := strings.TrimSpace(out.String())
	if got != "[]" {
		t.Fatalf("empty JSON = %q, want []", got)
	}
}

func TestListRunEmptyResultsText(t *testing.T) {
	t.Setenv("GC_TOKEN", "test-token")

	io, _, out, _ := iostreams.Test()
	opts := &ListOptions{
		IO: io,
		HttpClient: func() (*http.Client, error) {
			return &http.Client{
				Transport: testutil.NewRoundTripFunc(func(req *http.Request) (*http.Response, error) {
					return listTestResponse(http.StatusOK, `{"total_count":0,"runners":[]}`), nil
				}),
			}, nil
		},
		Repository: "owner/repo",
		Limit:      30,
	}

	if err := listRun(opts); err != nil {
		t.Fatalf("listRun() error = %v", err)
	}

	got := strings.TrimSpace(out.String())
	if got != "No shared runners found" {
		t.Fatalf("empty text = %q, want 'No shared runners found'", got)
	}
}

func TestListRunTextOutput(t *testing.T) {
	t.Setenv("GC_TOKEN", "test-token")

	io, _, out, _ := iostreams.Test()
	opts := &ListOptions{
		IO: io,
		HttpClient: func() (*http.Client, error) {
			return &http.Client{
				Transport: testutil.NewRoundTripFunc(func(req *http.Request) (*http.Response, error) {
					return listTestResponse(http.StatusOK, `{"total_count":1,"runners":[{"id":"r1","runner_name":"shared-1","labels":[{"label_name":"linux"}]}]}`), nil
				}),
			}, nil
		},
		Repository: "owner/repo",
		Limit:      30,
	}

	if err := listRun(opts); err != nil {
		t.Fatalf("listRun() error = %v", err)
	}

	got := out.String()
	if !strings.Contains(got, "shared-1") {
		t.Fatalf("text output = %q, missing runner name", got)
	}
	if !strings.Contains(got, "linux") {
		t.Fatalf("text output = %q, missing label", got)
	}
}

func TestListRunInvalidLimit(t *testing.T) {
	t.Setenv("GC_TOKEN", "test-token")

	io, _, _, _ := iostreams.Test()
	opts := &ListOptions{
		IO:         io,
		HttpClient: func() (*http.Client, error) { return &http.Client{}, nil },
		Repository: "owner/repo",
		Limit:      0,
	}

	err := listRun(opts)
	if err == nil {
		t.Fatal("listRun() error = nil, want error for --limit <= 0")
	}
}

func TestListRunPaginateWithPage(t *testing.T) {
	t.Setenv("GC_TOKEN", "test-token")

	io, _, _, _ := iostreams.Test()
	opts := &ListOptions{
		IO:         io,
		HttpClient: func() (*http.Client, error) { return &http.Client{}, nil },
		Repository: "owner/repo",
		Limit:      30,
		Paginate:   true,
		Page:       1,
	}

	err := listRun(opts)
	if err == nil {
		t.Fatal("listRun() error = nil, want error for --paginate + --page")
	}
}

func TestListRunAPIError(t *testing.T) {
	t.Setenv("GC_TOKEN", "test-token")

	io, _, _, _ := iostreams.Test()
	opts := &ListOptions{
		IO: io,
		HttpClient: func() (*http.Client, error) {
			return &http.Client{
				Transport: testutil.NewRoundTripFunc(func(req *http.Request) (*http.Response, error) {
					return listTestResponse(http.StatusNotFound, `{"message":"not found"}`), nil
				}),
			}, nil
		},
		Repository: "owner/repo",
		Limit:      30,
	}

	err := listRun(opts)
	if err == nil {
		t.Fatal("listRun() error = nil, want error for 404")
	}
}

func TestListRunPaginateEmptyResultsEmitsEmptyArray(t *testing.T) {
	t.Setenv("GC_TOKEN", "test-token")

	io, _, out, _ := iostreams.Test()
	opts := &ListOptions{
		IO: io,
		HttpClient: func() (*http.Client, error) {
			return &http.Client{
				Transport: testutil.NewRoundTripFunc(func(req *http.Request) (*http.Response, error) {
					return listTestResponse(http.StatusOK, `{"total_count":0,"runners":[]}`), nil
				}),
			}, nil
		},
		Repository: "owner/repo",
		Limit:      30,
		Paginate:   true,
		PerPage:    100,
		PerPageSet: true,
		JSON:       true,
	}

	if err := listRun(opts); err != nil {
		t.Fatalf("listRun() error = %v", err)
	}

	got := strings.TrimSpace(out.String())
	if got != "[]" {
		t.Fatalf("paginate empty JSON = %q, want []", got)
	}
}

func TestListRunPaginateMultiplePages(t *testing.T) {
	t.Setenv("GC_TOKEN", "test-token")

	io, _, out, _ := iostreams.Test()
	pageCount := 0
	opts := &ListOptions{
		IO: io,
		HttpClient: func() (*http.Client, error) {
			return &http.Client{
				Transport: testutil.NewRoundTripFunc(func(req *http.Request) (*http.Response, error) {
					pageCount++
					if pageCount == 1 {
						return listTestResponse(http.StatusOK, `{"total_count":2,"runners":[{"id":"r1","runner_name":"r1"}]}`), nil
					}
					return listTestResponse(http.StatusOK, `{"total_count":2,"runners":[]}`), nil
				}),
			}, nil
		},
		Repository: "owner/repo",
		Limit:      30,
		Paginate:   true,
		PerPage:    1,
		PerPageSet: true,
		JSON:       true,
	}

	if err := listRun(opts); err != nil {
		t.Fatalf("listRun() error = %v", err)
	}

	if pageCount != 2 {
		t.Fatalf("page count = %d, want 2", pageCount)
	}

	got := strings.TrimSpace(out.String())
	if !strings.Contains(got, `"id": "r1"`) {
		t.Fatalf("JSON output = %q, missing id from page 1", got)
	}
}

func TestTrimRunners(t *testing.T) {
	opts := &ListOptions{Limit: 1, PerPage: 10, LimitSet: true, PerPageSet: true}
	tests := []struct {
		name    string
		runners []api.Runner
		opts    *ListOptions
		want    int
	}{
		{name: "nil returns empty", runners: nil, opts: &ListOptions{}, want: 0},
		{name: "trim to limit", runners: make([]api.Runner, 5), opts: opts, want: 1},
		{name: "no trim", runners: make([]api.Runner, 5), opts: &ListOptions{Limit: 1, LimitSet: true}, want: 5},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := trimRunners(tt.runners, tt.opts)
			if len(got) != tt.want {
				t.Fatalf("trimRunners() len = %d, want %d", len(got), tt.want)
			}
		})
	}
}

func listTestResponse(status int, body string) *http.Response {
	return &http.Response{
		StatusCode: status,
		Status:     http.StatusText(status),
		Header:     make(http.Header),
		Body:       io.NopCloser(strings.NewReader(body)),
	}
}

func assertNoAccessTokenQuery(t *testing.T, path string) {
	t.Helper()
	if strings.Contains(path, "access_token=") {
		t.Fatalf("request path unexpectedly contains access_token query: %q", path)
	}
}