package list

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/output"
	"gitcode.com/gitcode-cli/cli/pkg/testutil"
)

func TestNewCmdList(t *testing.T) {
	tests := []struct {
		name    string
		args    []string
		wantErr bool
	}{
		{name: "list with org", args: []string{"--org", "my-org"}, wantErr: false},
		{name: "list with keyword", args: []string{"--org", "my-org", "--keyword", "prod"}, wantErr: false},
		{name: "list with limit", args: []string{"--org", "my-org", "--limit", "10"}, wantErr: false},
		{name: "list with paginate", args: []string{"--org", "my-org", "--paginate", "--per-page", "100"}, wantErr: false},
		{name: "list with page", args: []string{"--org", "my-org", "--page", "2"}, wantErr: false},
		{name: "list with json", args: []string{"--org", "my-org", "--json"}, wantErr: false},
		{name: "list with table format", args: []string{"--org", "my-org", "--format", "table"}, wantErr: false},
		{name: "missing org", args: []string{}, wantErr: true},
	}

	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,"runner_groups":[]}`), nil
				}),
			}, nil
		},
		Org:     "my-org",
		Keyword: "prod",
		Limit:   25,
		Page:    2,
	}

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

	if !strings.HasPrefix(gotPath, "/api/v8/orgs/my-org/actions/runner-groups?") {
		t.Fatalf("request path = %q, want v8 orgs runner-groups 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"))
	}
	if q.Get("per_page") != "25" {
		t.Fatalf("per_page = %q, want 25", q.Get("per_page"))
	}
	if q.Get("page") != "2" {
		t.Fatalf("page = %q, want 2", q.Get("page"))
	}
}

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

	io, _, _, _ := iostreams.Test()
	var gotPath string
	opts := &ListOptions{
		IO: io,
		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,"runner_groups":[]}`), nil
				}),
			}, nil
		},
		Org:   "my-org",
		Limit: 30,
	}

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

	if gotPath != "/api/v8/orgs/my-org/actions/runner-groups?per_page=30" {
		t.Fatalf("request path = %q, want default per_page=30", gotPath)
	}
}

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,"runner_groups":[{"id":"1","name":"prod","runner_group_name":"prod","namespace_id":"10","creator":"admin","create_time":1700000000,"runner_count":3,"namespace_type":"Organization","share_all":true}]}`), nil
				}),
			}, nil
		},
		Org:   "my-org",
		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": "1"`) {
		t.Fatalf("JSON output = %q, missing id field", got)
	}
	if !strings.Contains(got, `"runner_group_name": "prod"`) {
		t.Fatalf("JSON output = %q, missing runner_group_name", got)
	}
	if !strings.Contains(got, `"share_all": true`) {
		t.Fatalf("JSON output = %q, missing share_all", 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,"runner_groups":[]}`), nil
				}),
			}, nil
		},
		Org:   "my-org",
		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 output = %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,"runner_groups":[]}`), nil
				}),
			}, nil
		},
		Org:   "my-org",
		Limit: 30,
	}

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

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

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,"runner_groups":[]}`), nil
				}),
			}, nil
		},
		Org:        "my-org",
		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 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,"runner_groups":[{"id":"1","name":"prod","runner_group_name":"prod-group","namespace_id":"10","creator":"admin","create_time":1700000000,"runner_count":3,"namespace_type":"Organization","share_all":true}]}`), nil
				}),
			}, nil
		},
		Org:   "my-org",
		Limit: 30,
	}

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

	got := out.String()
	if !strings.Contains(got, "prod-group") {
		t.Fatalf("text output = %q, missing group name", got)
	}
	if !strings.Contains(got, "runners=3") {
		t.Fatalf("text output = %q, missing runner count", got)
	}
	if !strings.Contains(got, "shared-all") {
		t.Fatalf("text output = %q, missing share status", got)
	}
}

func TestListRunMissingOrg(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 },
		Org:        "",
		Limit:      30,
	}

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

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 },
		Org:        "my-org",
		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 },
		Org:        "my-org",
		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
		},
		Org:   "nonexistent",
		Limit: 30,
	}

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

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,"runner_groups":[{"id":"1","name":"g1","runner_group_name":"g1","runner_count":1,"share_all":false}]}`), nil
					}
					return listTestResponse(http.StatusOK, `{"total_count":2,"runner_groups":[]}`), nil
				}),
			}, nil
		},
		Org:        "my-org",
		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": "1"`) {
		t.Fatalf("JSON output = %q, missing id from page 1", got)
	}
}

func TestResolveOutputFormat(t *testing.T) {
	tests := []struct {
		name     string
		jsonFlag bool
		raw      string
		want     output.Format
		wantErr  bool
	}{
		{name: "default", jsonFlag: false, raw: "", want: output.FormatSimple},
		{name: "json flag", jsonFlag: true, want: output.FormatJSON},
		{name: "json with format json", jsonFlag: true, raw: "json", want: output.FormatJSON},
		{name: "invalid format", raw: "yaml", wantErr: true},
		{name: "json incompatible format", jsonFlag: true, raw: "table", wantErr: true},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := resolveOutputFormat(tt.jsonFlag, tt.raw)
			if (err != nil) != tt.wantErr {
				t.Fatalf("resolveOutputFormat() error = %v, wantErr %v", err, tt.wantErr)
			}
			if err == nil && got != tt.want {
				t.Fatalf("resolveOutputFormat() = %q, want %q", got, tt.want)
			}
		})
	}
}

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

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := trimGroups(tt.groups, tt.opts)
			if len(got) != tt.want {
				t.Fatalf("trimGroups() 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)
	}
}