package delete

import (
	"bytes"
	"net/http"
	"net/url"
	"testing"
	"time"

	"github.com/cli/cli/v2/internal/ghrepo"
	"github.com/cli/cli/v2/pkg/cmd/cache/shared"
	"github.com/cli/cli/v2/pkg/cmdutil"
	"github.com/cli/cli/v2/pkg/httpmock"
	"github.com/cli/cli/v2/pkg/iostreams"
	"github.com/google/shlex"
	"github.com/stretchr/testify/assert"
)

func TestNewCmdDelete(t *testing.T) {
	tests := []struct {
		name     string
		cli      string
		wants    DeleteOptions
		wantsErr string
	}{
		{
			name:     "no arguments",
			cli:      "",
			wantsErr: "must provide either cache id, cache key, or use --all",
		},
		{
			name:  "id argument",
			cli:   "123",
			wants: DeleteOptions{Identifier: "123"},
		},
		{
			name:  "key argument",
			cli:   "A-Cache-Key",
			wants: DeleteOptions{Identifier: "A-Cache-Key"},
		},
		{
			name:  "delete all flag",
			cli:   "--all",
			wants: DeleteOptions{DeleteAll: true},
		},
		{
			name:  "delete all and succeed-on-no-caches flags",
			cli:   "--all --succeed-on-no-caches",
			wants: DeleteOptions{DeleteAll: true, SucceedOnNoCaches: true},
		},
		{
			name:     "succeed-on-no-caches flag",
			cli:      "--succeed-on-no-caches",
			wantsErr: "--succeed-on-no-caches must be used in conjunction with --all",
		},
		{
			name:     "succeed-on-no-caches flag and id argument",
			cli:      "--succeed-on-no-caches 123",
			wantsErr: "--succeed-on-no-caches must be used in conjunction with --all",
		},
		{
			name:     "key argument and delete all flag",
			cli:      "cache-key --all",
			wantsErr: "specify only one of cache id, cache key, or --all",
		},
		{
			name:     "id argument and delete all flag",
			cli:      "1 --all",
			wantsErr: "specify only one of cache id, cache key, or --all",
		},
		{
			name:  "key argument with ref",
			cli:   "cache-key --ref refs/heads/main",
			wants: DeleteOptions{Identifier: "cache-key", Ref: "refs/heads/main"},
		},
		{
			name:     "ref flag without cache key",
			cli:      "--ref refs/heads/main",
			wantsErr: "must provide a cache key",
		},
		{
			name:     "ref flag with cache id",
			cli:      "123 --ref refs/heads/main",
			wantsErr: "--ref cannot be used with cache ID",
		},
		{
			name:  "ref flag with all flag",
			cli:   "--all --ref refs/heads/main",
			wants: DeleteOptions{DeleteAll: true, Ref: "refs/heads/main"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			f := &cmdutil.Factory{}
			argv, err := shlex.Split(tt.cli)
			assert.NoError(t, err)
			var gotOpts *DeleteOptions
			cmd := NewCmdDelete(f, func(opts *DeleteOptions) error {
				gotOpts = opts
				return nil
			})
			cmd.SetArgs(argv)
			cmd.SetIn(&bytes.Buffer{})
			cmd.SetOut(&bytes.Buffer{})
			cmd.SetErr(&bytes.Buffer{})

			_, err = cmd.ExecuteC()
			if tt.wantsErr != "" {
				assert.EqualError(t, err, tt.wantsErr)
				return
			}
			assert.NoError(t, err)
			assert.Equal(t, tt.wants.DeleteAll, gotOpts.DeleteAll)
			assert.Equal(t, tt.wants.SucceedOnNoCaches, gotOpts.SucceedOnNoCaches)
			assert.Equal(t, tt.wants.Identifier, gotOpts.Identifier)
			assert.Equal(t, tt.wants.Ref, gotOpts.Ref)
		})
	}
}

func TestDeleteRun(t *testing.T) {
	tests := []struct {
		name       string
		opts       DeleteOptions
		stubs      func(*httpmock.Registry)
		tty        bool
		wantErr    bool
		wantErrMsg string
		wantStderr string
		wantStdout string
	}{
		{
			name: "deletes cache tty",
			opts: DeleteOptions{Identifier: "123"},
			stubs: func(reg *httpmock.Registry) {
				reg.Register(
					httpmock.REST("DELETE", "repos/OWNER/REPO/actions/caches/123"),
					httpmock.StatusStringResponse(204, ""),
				)
			},
			tty:        true,
			wantStdout: "✓ Deleted 1 cache from OWNER/REPO\n",
		},
		{
			name: "deletes cache notty",
			opts: DeleteOptions{Identifier: "123"},
			stubs: func(reg *httpmock.Registry) {
				reg.Register(
					httpmock.REST("DELETE", "repos/OWNER/REPO/actions/caches/123"),
					httpmock.StatusStringResponse(204, ""),
				)
			},
			tty:        false,
			wantStdout: "",
		},
		{
			name: "non-existent cache",
			opts: DeleteOptions{Identifier: "123"},
			stubs: func(reg *httpmock.Registry) {
				reg.Register(
					httpmock.REST("DELETE", "repos/OWNER/REPO/actions/caches/123"),
					httpmock.StatusStringResponse(404, ""),
				)
			},
			wantErr:    true,
			wantErrMsg: "X Could not find a cache matching 123 in OWNER/REPO",
		},
		{
			name: "deletes all caches",
			opts: DeleteOptions{DeleteAll: true},
			stubs: func(reg *httpmock.Registry) {
				reg.Register(
					httpmock.REST("GET", "repos/OWNER/REPO/actions/caches"),
					httpmock.JSONResponse(shared.CachePayload{
						ActionsCaches: []shared.Cache{
							{
								Id:             123,
								Key:            "foo",
								CreatedAt:      time.Date(2021, 1, 1, 1, 1, 1, 1, time.UTC),
								LastAccessedAt: time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),
							},
							{
								Id:             456,
								Key:            "bar",
								CreatedAt:      time.Date(2021, 1, 1, 1, 1, 1, 1, time.UTC),
								LastAccessedAt: time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),
							},
						},
						TotalCount: 2,
					}),
				)
				reg.Register(
					httpmock.REST("DELETE", "repos/OWNER/REPO/actions/caches/123"),
					httpmock.StatusStringResponse(204, ""),
				)
				reg.Register(
					httpmock.REST("DELETE", "repos/OWNER/REPO/actions/caches/456"),
					httpmock.StatusStringResponse(204, ""),
				)
			},
			tty:        true,
			wantStdout: "✓ Deleted 2 caches from OWNER/REPO\n",
		},
		{
			name: "attempts to delete all caches but api errors",
			opts: DeleteOptions{DeleteAll: true},
			stubs: func(reg *httpmock.Registry) {
				reg.Register(
					httpmock.REST("GET", "repos/OWNER/REPO/actions/caches"),
					httpmock.StatusStringResponse(500, ""),
				)
			},
			tty:        true,
			wantErr:    true,
			wantErrMsg: "HTTP 500 (https://api.github.com/repos/OWNER/REPO/actions/caches?per_page=100)",
		},
		{
			name: "displays delete error",
			opts: DeleteOptions{Identifier: "123"},
			stubs: func(reg *httpmock.Registry) {
				reg.Register(
					httpmock.REST("DELETE", "repos/OWNER/REPO/actions/caches/123"),
					httpmock.StatusStringResponse(500, ""),
				)
			},
			wantErr:    true,
			wantErrMsg: "X Failed to delete cache: HTTP 500 (https://api.github.com/repos/OWNER/REPO/actions/caches/123)",
		},
		{
			name: "keys must be percent-encoded before being used as query params",
			opts: DeleteOptions{Identifier: "a weird＿cache+key"},
			stubs: func(reg *httpmock.Registry) {
				reg.Register(
					httpmock.QueryMatcher("DELETE", "repos/OWNER/REPO/actions/caches", url.Values{
						"key": []string{"a weird＿cache+key"},
					}),
					httpmock.JSONResponse(shared.CachePayload{
						TotalCount: 1,
					}),
				)
			},
			tty:        true,
			wantStdout: "✓ Deleted 1 cache from OWNER/REPO\n",
		},
		{
			name: "deletes multiple caches by key",
			opts: DeleteOptions{Identifier: "shared-cache-key"},
			stubs: func(reg *httpmock.Registry) {
				reg.Register(
					httpmock.QueryMatcher("DELETE", "repos/OWNER/REPO/actions/caches", url.Values{
						"key": []string{"shared-cache-key"},
					}),
					httpmock.JSONResponse(shared.CachePayload{
						TotalCount: 5,
					}),
				)
			},
			tty:        true,
			wantStdout: "✓ Deleted 5 caches from OWNER/REPO\n",
		},
		{
			name: "no caches to delete when deleting all",
			opts: DeleteOptions{DeleteAll: true},
			stubs: func(reg *httpmock.Registry) {
				reg.Register(
					httpmock.REST("GET", "repos/OWNER/REPO/actions/caches"),
					httpmock.JSONResponse(shared.CachePayload{
						ActionsCaches: []shared.Cache{},
						TotalCount:    0,
					}),
				)
			},
			tty:        false,
			wantErr:    true,
			wantErrMsg: "X No caches to delete",
		},
		{
			name: "no caches to delete when deleting all but succeed on no cache tty",
			opts: DeleteOptions{DeleteAll: true, SucceedOnNoCaches: true},
			stubs: func(reg *httpmock.Registry) {
				reg.Register(
					httpmock.REST("GET", "repos/OWNER/REPO/actions/caches"),
					httpmock.JSONResponse(shared.CachePayload{
						ActionsCaches: []shared.Cache{},
						TotalCount:    0,
					}),
				)
			},
			tty:        true,
			wantErr:    false,
			wantStdout: "✓ No caches to delete\n",
		},
		{
			name: "no caches to delete when deleting all but succeed on no cache non-tty",
			opts: DeleteOptions{DeleteAll: true, SucceedOnNoCaches: true},
			stubs: func(reg *httpmock.Registry) {
				reg.Register(
					httpmock.REST("GET", "repos/OWNER/REPO/actions/caches"),
					httpmock.JSONResponse(shared.CachePayload{
						ActionsCaches: []shared.Cache{},
						TotalCount:    0,
					}),
				)
			},
			tty:        false,
			wantErr:    false,
			wantStdout: "",
		},
		{
			name: "deletes cache with ref tty",
			opts: DeleteOptions{Identifier: "cache-key", Ref: "refs/heads/main"},
			stubs: func(reg *httpmock.Registry) {
				reg.Register(
					httpmock.QueryMatcher("DELETE", "repos/OWNER/REPO/actions/caches", url.Values{
						"key": []string{"cache-key"},
						"ref": []string{"refs/heads/main"},
					}),
					httpmock.JSONResponse(shared.CachePayload{
						TotalCount: 1,
					}),
				)
			},
			tty:        true,
			wantStdout: "✓ Deleted 1 cache from OWNER/REPO\n",
		},
		{
			name: "deletes cache with ref non-tty",
			opts: DeleteOptions{Identifier: "cache-key", Ref: "refs/heads/main"},
			stubs: func(reg *httpmock.Registry) {
				reg.Register(
					httpmock.QueryMatcher("DELETE", "repos/OWNER/REPO/actions/caches", url.Values{
						"key": []string{"cache-key"},
						"ref": []string{"refs/heads/main"},
					}),
					httpmock.JSONResponse(shared.CachePayload{
						TotalCount: 1,
					}),
				)
			},
			tty:        false,
			wantStdout: "",
		},
		{
			name: "deletes multiple caches by key and ref",
			opts: DeleteOptions{Identifier: "cache-key", Ref: "refs/heads/feature"},
			stubs: func(reg *httpmock.Registry) {
				reg.Register(
					httpmock.QueryMatcher("DELETE", "repos/OWNER/REPO/actions/caches", url.Values{
						"key": []string{"cache-key"},
						"ref": []string{"refs/heads/feature"},
					}),
					httpmock.JSONResponse(shared.CachePayload{
						TotalCount: 3,
					}),
				)
			},
			tty:        true,
			wantStdout: "✓ Deleted 3 caches from OWNER/REPO\n",
		},
		{
			// As of now, the API returns HTTP 404 for invalid or non-existent refs.
			name: "cache key exists but ref is invalid/not-found",
			opts: DeleteOptions{Identifier: "existing-cache-key", Ref: "invalid-ref"},
			stubs: func(reg *httpmock.Registry) {
				reg.Register(
					httpmock.QueryMatcher("DELETE", "repos/OWNER/REPO/actions/caches", url.Values{
						"key": []string{"existing-cache-key"},
						"ref": []string{"invalid-ref"},
					}),
					httpmock.StatusStringResponse(404, ""),
				)
			},
			wantErr:    true,
			wantErrMsg: "X Could not find a cache matching existing-cache-key (with ref invalid-ref) in OWNER/REPO",
		},
		{
			name: "deletes all caches with ref",
			opts: DeleteOptions{DeleteAll: true, Ref: "refs/heads/main"},
			stubs: func(reg *httpmock.Registry) {
				reg.Register(
					httpmock.QueryMatcher("GET", "repos/OWNER/REPO/actions/caches", url.Values{
						"ref": []string{"refs/heads/main"},
					}),
					httpmock.JSONResponse(shared.CachePayload{
						ActionsCaches: []shared.Cache{
							{
								Id:             123,
								Key:            "foo",
								Ref:            "refs/heads/main",
								CreatedAt:      time.Date(2021, 1, 1, 1, 1, 1, 1, time.UTC),
								LastAccessedAt: time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),
							},
							{
								Id:             456,
								Key:            "bar",
								Ref:            "refs/heads/main",
								CreatedAt:      time.Date(2021, 1, 1, 1, 1, 1, 1, time.UTC),
								LastAccessedAt: time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),
							},
						},
						TotalCount: 2,
					}),
				)
				reg.Register(
					httpmock.REST("DELETE", "repos/OWNER/REPO/actions/caches/123"),
					httpmock.StatusStringResponse(204, ""),
				)
				reg.Register(
					httpmock.REST("DELETE", "repos/OWNER/REPO/actions/caches/456"),
					httpmock.StatusStringResponse(204, ""),
				)
			},
			tty:        true,
			wantStdout: "✓ Deleted 2 caches from OWNER/REPO\n",
		},
		{
			name: "no caches to delete when deleting all with ref",
			opts: DeleteOptions{DeleteAll: true, Ref: "refs/heads/main"},
			stubs: func(reg *httpmock.Registry) {
				reg.Register(
					httpmock.QueryMatcher("GET", "repos/OWNER/REPO/actions/caches", url.Values{
						"ref": []string{"refs/heads/main"},
					}),
					httpmock.JSONResponse(shared.CachePayload{
						ActionsCaches: []shared.Cache{},
						TotalCount:    0,
					}),
				)
			},
			tty:        false,
			wantErr:    true,
			wantErrMsg: "X No caches to delete",
		},
		{
			name: "no caches to delete when deleting all for ref but succeed on no cache tty",
			opts: DeleteOptions{DeleteAll: true, SucceedOnNoCaches: true, Ref: "refs/heads/main"},
			stubs: func(reg *httpmock.Registry) {
				reg.Register(
					httpmock.QueryMatcher("GET", "repos/OWNER/REPO/actions/caches", url.Values{
						"ref": []string{"refs/heads/main"},
					}),
					httpmock.JSONResponse(shared.CachePayload{
						ActionsCaches: []shared.Cache{},
						TotalCount:    0,
					}),
				)
			},
			tty:        true,
			wantErr:    false,
			wantStdout: "✓ No caches to delete\n",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			reg := &httpmock.Registry{}
			if tt.stubs != nil {
				tt.stubs(reg)
			}
			tt.opts.HttpClient = func() (*http.Client, error) {
				return &http.Client{Transport: reg}, nil
			}
			ios, _, stdout, stderr := iostreams.Test()
			ios.SetStdoutTTY(tt.tty)
			ios.SetStdinTTY(tt.tty)
			ios.SetStderrTTY(tt.tty)
			tt.opts.IO = ios
			tt.opts.BaseRepo = func() (ghrepo.Interface, error) {
				return ghrepo.New("OWNER", "REPO"), nil
			}
			defer reg.Verify(t)

			err := deleteRun(&tt.opts)
			if tt.wantErr {
				if tt.wantErrMsg != "" {
					assert.EqualError(t, err, tt.wantErrMsg)
				} else {
					assert.Error(t, err)
				}
			} else {
				assert.NoError(t, err)
			}
			assert.Equal(t, tt.wantStdout, stdout.String())
			assert.Equal(t, tt.wantStderr, stderr.String())
		})
	}
}
