package cmd

import (
	"context"
	"fmt"
	"net/http"
	"os"
	"path/filepath"
	"testing"
	"time"

	"github.com/SAP/jenkins-library/cmd/mocks"
	"github.com/google/go-github/v45/github"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
)

type ghRCMock struct {
	createErr         error
	latestRelease     *github.RepositoryRelease
	release           *github.RepositoryRelease
	delErr            error
	delID             int64
	delOwner          string
	delRepo           string
	listErr           error
	listID            int64
	listOwner         string
	listReleaseAssets []*github.ReleaseAsset
	listRepo          string
	listOpts          *github.ListOptions
	latestStatusCode  int
	latestErr         error
	uploadID          int64
	uploadOpts        *github.UploadOptions
	uploadOwner       string
	uploadRepo        string
}

func (g *ghRCMock) CreateRelease(ctx context.Context, owner string, repo string, release *github.RepositoryRelease) (*github.RepositoryRelease, *github.Response, error) {
	g.release = release
	return release, nil, g.createErr
}

func (g *ghRCMock) DeleteReleaseAsset(ctx context.Context, owner string, repo string, id int64) (*github.Response, error) {
	g.delOwner = owner
	g.delRepo = repo
	g.delID = id
	return nil, g.delErr
}

func (g *ghRCMock) GetLatestRelease(ctx context.Context, owner string, repo string) (*github.RepositoryRelease, *github.Response, error) {
	hc := http.Response{StatusCode: 200}
	if g.latestStatusCode != 0 {
		hc.StatusCode = g.latestStatusCode
	}

	if len(owner) == 0 {
		return g.latestRelease, nil, g.latestErr
	}

	ghResp := github.Response{Response: &hc}
	return g.latestRelease, &ghResp, g.latestErr
}

func (g *ghRCMock) ListReleaseAssets(ctx context.Context, owner string, repo string, id int64, opt *github.ListOptions) ([]*github.ReleaseAsset, *github.Response, error) {
	g.listID = id
	g.listOwner = owner
	g.listRepo = repo
	g.listOpts = opt
	return g.listReleaseAssets, nil, g.listErr
}

func (g *ghRCMock) UploadReleaseAsset(ctx context.Context, owner string, repo string, id int64, opt *github.UploadOptions, file *os.File) (*github.ReleaseAsset, *github.Response, error) {
	g.uploadID = id
	g.uploadOwner = owner
	g.uploadRepo = repo
	g.uploadOpts = opt
	return nil, nil, nil
}

type ghICMock struct {
	issues        []*github.Issue
	lastPublished time.Time
	owner         string
	repo          string
	options       *github.IssueListByRepoOptions
}

func (g *ghICMock) ListByRepo(ctx context.Context, owner string, repo string, opt *github.IssueListByRepoOptions) ([]*github.Issue, *github.Response, error) {
	g.owner = owner
	g.repo = repo
	g.options = opt
	g.lastPublished = opt.Since
	return g.issues, nil, nil
}

func TestRunGithubPublishRelease(t *testing.T) {
	ctx := context.Background()

	t.Run("Success - first release & no body", func(t *testing.T) {
		ghIssueClient := ghICMock{}
		ghRepoClient := ghRCMock{
			latestStatusCode: 404,
			latestErr:        fmt.Errorf("not found"),
		}

		myGithubPublishReleaseOptions := githubPublishReleaseOptions{
			AddDeltaToLastRelease: true,
			Commitish:             "master",
			Owner:                 "TEST",
			PreRelease:            true,
			Repository:            "test",
			ServerURL:             "https://github.com",
			ReleaseBodyHeader:     "Header",
			Version:               "1.0",
		}
		err := runGithubPublishRelease(ctx, &myGithubPublishReleaseOptions, &ghRepoClient, &ghIssueClient)
		assert.NoError(t, err, "Error occurred but none expected.")

		assert.Equal(t, "Header\n", ghRepoClient.release.GetBody())
		assert.Equal(t, true, ghRepoClient.release.GetPrerelease())
		assert.Equal(t, "1.0", ghRepoClient.release.GetTagName())
	})

	t.Run("Success - first release with tag prefix set & no body", func(t *testing.T) {
		ghIssueClient := ghICMock{}
		ghRepoClient := ghRCMock{
			latestStatusCode: 404,
			latestErr:        fmt.Errorf("not found"),
		}

		myGithubPublishReleaseOptions := githubPublishReleaseOptions{
			AddDeltaToLastRelease: true,
			Commitish:             "master",
			Owner:                 "TEST",
			PreRelease:            true,
			Repository:            "test",
			ServerURL:             "https://github.com",
			ReleaseBodyHeader:     "Header",
			Version:               "1.0",
			TagPrefix:             "v",
		}
		err := runGithubPublishRelease(ctx, &myGithubPublishReleaseOptions, &ghRepoClient, &ghIssueClient)
		assert.NoError(t, err, "Error occurred but none expected.")

		assert.Equal(t, "Header\n", ghRepoClient.release.GetBody())
		assert.Equal(t, true, ghRepoClient.release.GetPrerelease())
		assert.Equal(t, "v1.0", ghRepoClient.release.GetTagName())
	})

	t.Run("Success - subsequent releases & with body", func(t *testing.T) {
		lastTag := "1.0"
		lastPublishedAt := github.Timestamp{Time: time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC)}
		ghRepoClient := ghRCMock{
			createErr: nil,
			latestRelease: &github.RepositoryRelease{
				TagName:     &lastTag,
				PublishedAt: &lastPublishedAt,
			},
		}
		prHTMLURL := "https://github.com/TEST/test/pull/1"
		prTitle := "Pull"
		prNo := 1

		issHTMLURL := "https://github.com/TEST/test/issues/2"
		issTitle := "Issue"
		issNo := 2

		ghIssueClient := ghICMock{
			issues: []*github.Issue{
				{Number: &prNo, Title: &prTitle, HTMLURL: &prHTMLURL, PullRequestLinks: &github.PullRequestLinks{URL: &prHTMLURL}},
				{Number: &issNo, Title: &issTitle, HTMLURL: &issHTMLURL},
			},
		}
		myGithubPublishReleaseOptions := githubPublishReleaseOptions{
			AddClosedIssues:       true,
			AddDeltaToLastRelease: true,
			Commitish:             "master",
			Owner:                 "TEST",
			Repository:            "test",
			ServerURL:             "https://github.com",
			ReleaseBodyHeader:     "Header",
			Version:               "1.1",
		}
		err := runGithubPublishRelease(ctx, &myGithubPublishReleaseOptions, &ghRepoClient, &ghIssueClient)

		assert.NoError(t, err, "Error occurred but none expected.")

		assert.Equal(t, "Header\n\n**List of closed pull-requests since last release**\n[#1](https://github.com/TEST/test/pull/1): Pull\n\n**List of closed issues since last release**\n[#2](https://github.com/TEST/test/issues/2): Issue\n\n**Changes**\n[1.0...1.1](https://github.com/TEST/test/compare/1.0...1.1)\n", ghRepoClient.release.GetBody())
		assert.Equal(t, "1.1", ghRepoClient.release.GetName())
		assert.Equal(t, "1.1", ghRepoClient.release.GetTagName())
		assert.Equal(t, "master", ghRepoClient.release.GetTargetCommitish())

		assert.Equal(t, lastPublishedAt.Time, ghIssueClient.lastPublished)
	})

	t.Run("Success - update asset", func(t *testing.T) {
		var releaseID int64 = 1
		ghIssueClient := ghICMock{}
		ghRepoClient := ghRCMock{
			latestRelease: &github.RepositoryRelease{
				ID: &releaseID,
			},
		}

		myGithubPublishReleaseOptions := githubPublishReleaseOptions{
			AssetPath: filepath.Join("testdata", t.Name()+"_test.txt"),
			Version:   "latest",
		}

		err := runGithubPublishRelease(ctx, &myGithubPublishReleaseOptions, &ghRepoClient, &ghIssueClient)

		assert.NoError(t, err, "Error occurred but none expected.")

		assert.Nil(t, ghRepoClient.release)

		assert.Equal(t, releaseID, ghRepoClient.listID)
		assert.Equal(t, releaseID, ghRepoClient.uploadID)
	})

	t.Run("Error - get release", func(t *testing.T) {
		ghIssueClient := ghICMock{}
		ghRepoClient := ghRCMock{
			latestErr: fmt.Errorf("Latest release error"),
		}
		myGithubPublishReleaseOptions := githubPublishReleaseOptions{
			Owner:      "TEST",
			Repository: "test",
		}
		err := runGithubPublishRelease(ctx, &myGithubPublishReleaseOptions, &ghRepoClient, &ghIssueClient)

		assert.Equal(t, "Error occurred when retrieving latest GitHub release (TEST/test): Latest release error", fmt.Sprint(err))
	})

	t.Run("Error - get release no response", func(t *testing.T) {
		ghIssueClient := ghICMock{}
		ghRepoClient := ghRCMock{
			latestErr: fmt.Errorf("Latest release error, no response"),
		}
		myGithubPublishReleaseOptions := githubPublishReleaseOptions{
			Owner:      "",
			Repository: "test",
		}
		err := runGithubPublishRelease(ctx, &myGithubPublishReleaseOptions, &ghRepoClient, &ghIssueClient)

		assert.Equal(t, "Error occurred when retrieving latest GitHub release (/test): Latest release error, no response", fmt.Sprint(err))
	})

	t.Run("Error - create release", func(t *testing.T) {
		ghIssueClient := ghICMock{}
		ghRepoClient := ghRCMock{
			createErr: fmt.Errorf("Create release error"),
		}
		myGithubPublishReleaseOptions := githubPublishReleaseOptions{
			Version: "1.0",
		}
		err := runGithubPublishRelease(ctx, &myGithubPublishReleaseOptions, &ghRepoClient, &ghIssueClient)

		assert.Equal(t, "Creation of release '1.0' failed: Create release error", fmt.Sprint(err))
	})
}

func TestGetClosedIssuesText(t *testing.T) {
	ctx := context.Background()
	publishedAt := github.Timestamp{Time: time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC)}

	t.Run("No issues", func(t *testing.T) {
		ghIssueClient := ghICMock{}
		myGithubPublishReleaseOptions := githubPublishReleaseOptions{
			Version: "1.0",
		}

		res := getClosedIssuesText(ctx, publishedAt, &myGithubPublishReleaseOptions, &ghIssueClient)

		assert.Equal(t, "", res)
	})

	t.Run("All issues", func(t *testing.T) {
		ctx := context.Background()
		publishedAt := github.Timestamp{Time: time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC)}

		prHTMLURL := []string{"https://github.com/TEST/test/pull/1", "https://github.com/TEST/test/pull/2"}
		prTitle := []string{"Pull1", "Pull2"}
		prNo := []int{1, 2}

		issHTMLURL := []string{"https://github.com/TEST/test/issues/3", "https://github.com/TEST/test/issues/4"}
		issTitle := []string{"Issue3", "Issue4"}
		issNo := []int{3, 4}

		ghIssueClient := ghICMock{
			issues: []*github.Issue{
				{Number: &prNo[0], Title: &prTitle[0], HTMLURL: &prHTMLURL[0], PullRequestLinks: &github.PullRequestLinks{URL: &prHTMLURL[0]}},
				{Number: &prNo[1], Title: &prTitle[1], HTMLURL: &prHTMLURL[1], PullRequestLinks: &github.PullRequestLinks{URL: &prHTMLURL[1]}},
				{Number: &issNo[0], Title: &issTitle[0], HTMLURL: &issHTMLURL[0]},
				{Number: &issNo[1], Title: &issTitle[1], HTMLURL: &issHTMLURL[1]},
			},
		}

		myGithubPublishReleaseOptions := githubPublishReleaseOptions{
			Owner:      "TEST",
			Repository: "test",
		}

		res := getClosedIssuesText(ctx, publishedAt, &myGithubPublishReleaseOptions, &ghIssueClient)

		assert.Equal(t, "\n**List of closed pull-requests since last release**\n[#1](https://github.com/TEST/test/pull/1): Pull1\n[#2](https://github.com/TEST/test/pull/2): Pull2\n\n**List of closed issues since last release**\n[#3](https://github.com/TEST/test/issues/3): Issue3\n[#4](https://github.com/TEST/test/issues/4): Issue4\n", res)
		assert.Equal(t, "TEST", ghIssueClient.owner, "Owner not properly passed")
		assert.Equal(t, "test", ghIssueClient.repo, "Repo not properly passed")
		assert.Equal(t, "closed", ghIssueClient.options.State, "Issue state not properly passed")
		assert.Equal(t, "asc", ghIssueClient.options.Direction, "Sort direction not properly passed")
		assert.Equal(t, publishedAt.Time, ghIssueClient.options.Since, "PublishedAt not properly passed")
	})
}

func TestGetReleaseDeltaText(t *testing.T) {
	myGithubPublishReleaseOptions := githubPublishReleaseOptions{
		Owner:      "TEST",
		Repository: "test",
		ServerURL:  "https://github.com",
		Version:    "1.1",
	}
	lastTag := "1.0"
	lastRelease := github.RepositoryRelease{
		TagName: &lastTag,
	}

	res := getReleaseDeltaText(&myGithubPublishReleaseOptions, &lastRelease)

	assert.Equal(t, "\n**Changes**\n[1.0...1.1](https://github.com/TEST/test/compare/1.0...1.1)\n", res)
}

func TestUploadReleaseAsset(t *testing.T) {
	ctx := context.Background()

	t.Run("Success - existing asset", func(t *testing.T) {
		var releaseID int64 = 1
		assetName := "Success_-_existing_asset_test.txt"
		var assetID int64 = 11
		ghRepoClient := ghRCMock{
			latestRelease: &github.RepositoryRelease{
				ID: &releaseID,
			},
			listReleaseAssets: []*github.ReleaseAsset{
				{Name: &assetName, ID: &assetID},
			},
		}

		myGithubPublishReleaseOptions := githubPublishReleaseOptions{
			Owner:      "TEST",
			Repository: "test",
			AssetPath:  filepath.Join("testdata", t.Name()+"_test.txt"),
		}

		err := uploadReleaseAsset(ctx, releaseID, &myGithubPublishReleaseOptions, &ghRepoClient)

		assert.NoError(t, err, "Error occurred but none expected.")

		assert.Equal(t, "TEST", ghRepoClient.listOwner, "Owner not properly passed - list")
		assert.Equal(t, "test", ghRepoClient.listRepo, "Repo not properly passed - list")
		assert.Equal(t, releaseID, ghRepoClient.listID, "Relase ID not properly passed - list")

		assert.Equal(t, "TEST", ghRepoClient.delOwner, "Owner not properly passed - del")
		assert.Equal(t, "test", ghRepoClient.delRepo, "Repo not properly passed - del")
		assert.Equal(t, assetID, ghRepoClient.delID, "Relase ID not properly passed - del")

		assert.Equal(t, "TEST", ghRepoClient.uploadOwner, "Owner not properly passed - upload")
		assert.Equal(t, "test", ghRepoClient.uploadRepo, "Repo not properly passed - upload")
		assert.Equal(t, releaseID, ghRepoClient.uploadID, "Relase ID not properly passed - upload")
		assert.Equal(t, "text/plain; charset=utf-8", ghRepoClient.uploadOpts.MediaType, "Wrong MediaType passed - upload")
	})

	t.Run("Success - no asset", func(t *testing.T) {
		var releaseID int64 = 1
		assetName := "notFound"
		var assetID int64 = 11
		ghRepoClient := ghRCMock{
			latestRelease: &github.RepositoryRelease{
				ID: &releaseID,
			},
			listReleaseAssets: []*github.ReleaseAsset{
				{Name: &assetName, ID: &assetID},
			},
		}

		myGithubPublishReleaseOptions := githubPublishReleaseOptions{
			Owner:      "TEST",
			Repository: "test",
			AssetPath:  filepath.Join("testdata", t.Name()+"_test.txt"),
		}

		err := uploadReleaseAsset(ctx, releaseID, &myGithubPublishReleaseOptions, &ghRepoClient)

		assert.NoError(t, err, "Error occurred but none expected.")

		assert.Equal(t, int64(0), ghRepoClient.delID, "Relase ID should not be populated")
	})

	t.Run("Error - List Assets", func(t *testing.T) {
		var releaseID int64 = 1
		ghRepoClient := ghRCMock{
			listErr: fmt.Errorf("List Asset Error"),
		}
		myGithubPublishReleaseOptions := githubPublishReleaseOptions{}

		err := uploadReleaseAsset(ctx, releaseID, &myGithubPublishReleaseOptions, &ghRepoClient)
		assert.Equal(t, "Failed to get list of release assets.: List Asset Error", fmt.Sprint(err), "Wrong error received")
	})
}

func TestUploadReleaseAssetList(t *testing.T) {
	ctx := context.Background()
	owner := "OWNER"
	repository := "REPOSITORY"
	var releaseID int64 = 1

	t.Run("Success - multiple asset", func(t *testing.T) {
		// init
		assetURL := mock.Anything
		asset1 := filepath.Join("testdata", t.Name()+"_1_test.txt")
		asset2 := filepath.Join("testdata", t.Name()+"_2_test.txt")
		assetName1 := filepath.Base(asset1)
		assetName2 := filepath.Base(asset2)
		var assetID1 int64 = 11
		var assetID2 int64 = 12
		stepConfig := githubPublishReleaseOptions{
			Owner:         owner,
			Repository:    repository,
			AssetPathList: []string{asset1, asset2},
		}
		// mocking
		ghClient := &mocks.GithubRepoClient{}
		ghClient.Test(t)
		ghClient.
			On("ListReleaseAssets", ctx, owner, repository, releaseID, mock.AnythingOfType("*github.ListOptions")).Return(
			[]*github.ReleaseAsset{
				{Name: &assetName1, ID: &assetID1, URL: &assetURL},
				{Name: &assetName2, ID: &assetID2, URL: &assetURL},
			},
			nil,
			nil,
		).
			On("DeleteReleaseAsset", ctx, owner, repository, mock.AnythingOfType("int64")).Return(
			&github.Response{Response: &http.Response{StatusCode: 200}},
			nil,
		).
			On("UploadReleaseAsset", ctx, owner, repository, releaseID, mock.AnythingOfType("*github.UploadOptions"), mock.AnythingOfType("*os.File")).Return(
			&github.ReleaseAsset{URL: &assetURL},
			&github.Response{Response: &http.Response{StatusCode: 200}},
			nil,
		)
		// test
		err := uploadReleaseAssetList(ctx, releaseID, &stepConfig, ghClient)
		// asserts
		assert.NoError(t, err)
		ghClient.AssertExpectations(t)
	})
}

func TestIsExcluded(t *testing.T) {
	l1 := "label1"
	l2 := "label2"

	tt := []struct {
		issue         *github.Issue
		excludeLabels []string
		expected      bool
	}{
		{issue: nil, excludeLabels: nil, expected: false},
		{issue: &github.Issue{}, excludeLabels: nil, expected: false},
		{issue: &github.Issue{Labels: []*github.Label{{Name: &l1}}}, excludeLabels: nil, expected: false},
		{issue: &github.Issue{Labels: []*github.Label{{Name: &l1}}}, excludeLabels: []string{"label0"}, expected: false},
		{issue: &github.Issue{Labels: []*github.Label{{Name: &l1}}}, excludeLabels: []string{"label1"}, expected: true},
		{issue: &github.Issue{Labels: []*github.Label{{Name: &l1}, {Name: &l2}}}, excludeLabels: []string{}, expected: false},
		{issue: &github.Issue{Labels: []*github.Label{{Name: &l1}, {Name: &l2}}}, excludeLabels: []string{"label1"}, expected: true},
	}

	for k, v := range tt {
		assert.Equal(t, v.expected, isExcluded(v.issue, v.excludeLabels), fmt.Sprintf("Run %v failed", k))
	}
}
