//go:build integration
// +build integration

package github

import (
	"encoding/base64"
	"fmt"
	"testing"
	"time"

	"github.com/kylelemons/godebug/pretty"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"google.golang.org/protobuf/types/known/anypb"

	"github.com/trufflesecurity/trufflehog/v3/pkg/cache/simple"
	"github.com/trufflesecurity/trufflehog/v3/pkg/common"
	"github.com/trufflesecurity/trufflehog/v3/pkg/context"
	"github.com/trufflesecurity/trufflehog/v3/pkg/feature"
	"github.com/trufflesecurity/trufflehog/v3/pkg/pb/credentialspb"
	"github.com/trufflesecurity/trufflehog/v3/pkg/pb/source_metadatapb"
	"github.com/trufflesecurity/trufflehog/v3/pkg/pb/sourcespb"
	"github.com/trufflesecurity/trufflehog/v3/pkg/sources"
)

func TestSource_Token(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second*300)
	defer cancel()

	secret, err := common.GetTestSecret(ctx)
	if err != nil {
		t.Fatal(fmt.Errorf("failed to access secret: %v", err))
	}

	githubPrivateKeyB64New := secret.MustGetField("GITHUB_PRIVATE_KEY_NEW")
	githubPrivateKeyBytesNew, err := base64.StdEncoding.DecodeString(githubPrivateKeyB64New)
	if err != nil {
		t.Fatal(err)
	}
	githubPrivateKeyNew := string(githubPrivateKeyBytesNew)
	githubInstallationIDNew := secret.MustGetField("GITHUB_INSTALLATION_ID_NEW")
	githubAppIDNew := secret.MustGetField("GITHUB_APP_ID_NEW")

	src := &sourcespb.GitHub{
		Endpoint: "https://api.github.com",
		Credential: &sourcespb.GitHub_GithubApp{
			GithubApp: &credentialspb.GitHubApp{
				PrivateKey:     githubPrivateKeyNew,
				InstallationId: githubInstallationIDNew,
				AppId:          githubAppIDNew,
			},
		},
	}
	conn, err := anypb.New(src)
	if err != nil {
		panic(err)
	}

	s := Source{
		conn:          src,
		memberCache:   map[string]struct{}{},
		repoInfoCache: newRepoInfoCache(),
	}
	s.Init(ctx, "github integration test source", 0, 0, false, conn, 1)
	s.filteredRepoCache = s.newFilteredRepoCache(ctx, simple.NewCache[string](), nil, nil)

	err = s.enumerateWithApp(ctx, s.connector.(*appConnector).InstallationClient(), noopReporter())
	assert.NoError(t, err)

	_, _, err = s.cloneRepo(ctx, "https://github.com/truffle-test-integration-org/another-test-repo.git")
	assert.NoError(t, err)
}

func TestSource_ScanComments(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
	defer cancel()

	secret, err := common.GetTestSecret(ctx)
	if err != nil {
		t.Fatal(fmt.Errorf("failed to access secret: %v", err))
	}

	// For the personal access token test
	githubToken := secret.MustGetField("GITHUB_TOKEN")

	const totalPRChunks = 3
	const totalIssueChunks = 2

	type init struct {
		name       string
		verify     bool
		connection *sourcespb.GitHub
	}
	tests := []struct {
		name              string
		init              init
		wantChunk         *sources.Chunk
		wantErr           bool
		minRepo           int
		minOrg            int
		numExpectedChunks int
	}{
		{
			name: "token authenticated, single repo, single issue comment",
			init: init{
				name: "test source",
				connection: &sourcespb.GitHub{
					Repositories:               []string{"https://github.com/truffle-test-integration-org/another-test-repo.git"},
					IncludeIssueComments:       true,
					IncludePullRequestComments: false,
					Credential: &sourcespb.GitHub_Token{
						Token: githubToken,
					},
				},
			},
			numExpectedChunks: totalIssueChunks,
			wantChunk: &sources.Chunk{
				SourceType: sourcespb.SourceType_SOURCE_TYPE_GITHUB,
				SourceName: "test source",
				SourceMetadata: &source_metadatapb.MetaData{
					Data: &source_metadatapb.MetaData_Github{
						Github: &source_metadatapb.Github{
							Link:      "https://github.com/truffle-test-integration-org/another-test-repo/issues/1",
							Username:  "truffle-sandbox",
							Timestamp: "2023-06-22 23:33:46 +0000 UTC",
						},
					},
				},
				Verify: false,
			},
			wantErr: false,
		},
		{
			name: "token authenticated, single repo, pull request comment",
			init: init{
				name: "test source",
				connection: &sourcespb.GitHub{
					Repositories:               []string{"https://github.com/truffle-test-integration-org/another-test-repo.git"},
					IncludePullRequestComments: true,
					IncludeIssueComments:       false,
					Credential: &sourcespb.GitHub_Token{
						Token: githubToken,
					},
				},
			},
			numExpectedChunks: totalPRChunks,
			wantChunk: &sources.Chunk{
				SourceType: sourcespb.SourceType_SOURCE_TYPE_GITHUB,
				SourceName: "test source",
				SourceMetadata: &source_metadatapb.MetaData{
					Data: &source_metadatapb.MetaData_Github{
						Github: &source_metadatapb.Github{
							Link:      "https://github.com/truffle-test-integration-org/another-test-repo/pull/2#discussion_r1242763304",
							Username:  "truffle-sandbox",
							Timestamp: "2023-06-26 21:00:11 +0000 UTC",
						},
					},
				},
				Verify: false,
			},
			wantErr: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s := Source{}

			conn, err := anypb.New(tt.init.connection)
			if err != nil {
				t.Fatal(err)
			}

			err = s.Init(ctx, tt.init.name, 0, 0, tt.init.verify, conn, 4)
			if (err != nil) != tt.wantErr {
				t.Errorf("Source.Init() error = %v, wantErr %v", err, tt.wantErr)
				return
			}

			if tt.wantErr {
				return
			}

			chunksCh := make(chan *sources.Chunk, 1)
			go func() {
				// Close the channel
				defer close(chunksCh)
				err = s.Chunks(ctx, chunksCh)
				if (err != nil) != tt.wantErr {
					if ctx.Err() != nil {
						return
					}
					t.Errorf("Source.Chunks() error = %v, wantErr %v", err, tt.wantErr)
					return
				}
			}()

			i := 0
			for gotChunk := range chunksCh {
				// Skip chunks that are not comments.
				if gotChunk.SourceMetadata.GetGithub().GetCommit() != "" {
					continue
				}
				i++
				githubCommentCheckFunc(gotChunk, tt.wantChunk, i, t, tt.name)
			}

			// Confirm all comments were processed.
			if i != tt.numExpectedChunks {
				t.Errorf("did not complete all chunks, got %d, want %d", i, tt.numExpectedChunks)
			}

		})
	}
}

func TestSource_ScanChunks(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
	defer cancel()

	secret, err := common.GetTestSecret(ctx)
	if err != nil {
		t.Fatal(fmt.Errorf("failed to access secret: %v", err))
	}

	// For the personal access token test.
	githubToken := secret.MustGetField("GITHUB_TOKEN")

	type init struct {
		name       string
		verify     bool
		connection *sourcespb.GitHub
	}
	tests := []struct {
		name       string
		init       init
		wantChunks int
	}{
		{
			name: "token authenticated, 4 repos",
			init: init{
				name: "test source",
				connection: &sourcespb.GitHub{
					Repositories: []string{
						"https://github.com/truffle-test-integration-org/another-test-repo.git",
						"https://github.com/trufflesecurity/trufflehog.git",
						"https://github.com/Akash-goyal-github/Inventory-Management-System.git",
						"https://github.com/R1ck404/Crypto-Exchange-Example.git",
						"https://github.com/Stability-AI/generative-models.git",
						"https://github.com/bloomberg/blazingmq.git",
						"https://github.com/Kong/kong.git",
					},
					Credential: &sourcespb.GitHub_Token{Token: githubToken},
				},
			},
			wantChunks: 20000,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s := Source{}

			conn, err := anypb.New(tt.init.connection)
			if err != nil {
				t.Fatal(err)
			}

			err = s.Init(ctx, tt.init.name, 0, 0, tt.init.verify, conn, 8)
			assert.Nil(t, err)

			chunksCh := make(chan *sources.Chunk, 1)
			go func() {
				defer close(chunksCh)
				err = s.Chunks(ctx, chunksCh)
				assert.Nil(t, err)
			}()

			i := 0
			for range chunksCh {
				i++
			}
			assert.GreaterOrEqual(t, i, tt.wantChunks)
		})
	}
}

func TestSource_Scan(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second*300)
	defer cancel()

	secret, err := common.GetTestSecret(ctx)
	if err != nil {
		t.Fatal(fmt.Errorf("failed to access secret: %v", err))
	}

	// For the personal access token test
	githubToken := secret.MustGetField("GITHUB_TOKEN")

	// For the  NEW github app test (+Member enum)
	githubPrivateKeyB64New := secret.MustGetField("GITHUB_PRIVATE_KEY_NEW")
	githubPrivateKeyBytesNew, err := base64.StdEncoding.DecodeString(githubPrivateKeyB64New)
	if err != nil {
		t.Fatal(err)
	}
	githubPrivateKeyNew := string(githubPrivateKeyBytesNew)
	githubInstallationIDNew := secret.MustGetField("GITHUB_INSTALLATION_ID_NEW")
	githubAppIDNew := secret.MustGetField("GITHUB_APP_ID_NEW")

	type init struct {
		name       string
		verify     bool
		connection *sourcespb.GitHub
	}
	tests := []struct {
		name      string
		init      init
		wantChunk *sources.Chunk
		wantErr   bool
		minRepo   int
		minOrg    int
	}{
		{
			name: "token authenticated, single repo",
			init: init{
				name: "test source",
				connection: &sourcespb.GitHub{
					Repositories: []string{"https://github.com/truffle-test-integration-org/another-test-repo.git"},
					Credential: &sourcespb.GitHub_Token{
						Token: githubToken,
					},
				},
			},
			wantChunk: &sources.Chunk{
				SourceType: sourcespb.SourceType_SOURCE_TYPE_GITHUB,
				SourceName: "test source",
				SourceMetadata: &source_metadatapb.MetaData{
					Data: &source_metadatapb.MetaData_Github{
						Github: &source_metadatapb.Github{
							Repository: "https://github.com/truffle-test-integration-org/another-test-repo.git",
						},
					},
				},
				Verify: false,
			},
			wantErr: false,
		},
		{
			name: "token authenticated, single repo, no .git",
			init: init{
				name: "test source",
				connection: &sourcespb.GitHub{
					Repositories: []string{"https://github.com/truffle-test-integration-org/another-test-repo"},
					Credential: &sourcespb.GitHub_Token{
						Token: githubToken,
					},
				},
			},
			wantChunk: &sources.Chunk{
				SourceType: sourcespb.SourceType_SOURCE_TYPE_GITHUB,
				SourceName: "test source",
				SourceMetadata: &source_metadatapb.MetaData{
					Data: &source_metadatapb.MetaData_Github{
						Github: &source_metadatapb.Github{
							Repository: "https://github.com/truffle-test-integration-org/another-test-repo.git",
						},
					},
				},
				Verify: false,
			},
			wantErr: false,
		},
		{
			name: "token authenticated, single org",
			init: init{
				name: "test source",
				connection: &sourcespb.GitHub{
					Organizations: []string{"truffle-test-integration-org"},
					Credential: &sourcespb.GitHub_Token{
						Token: githubToken,
					},
				},
			},
			wantChunk: nil,
			wantErr:   false,
			minRepo:   1,
			minOrg:    1,
		},
		{
			name: "token authenticated, username in org",
			init: init{
				name: "test source",
				connection: &sourcespb.GitHub{
					Organizations: []string{"truffle-sandbox"},
					Credential: &sourcespb.GitHub_Token{
						Token: githubToken,
					},
				},
			},
			wantChunk: nil,
			wantErr:   false,
			minRepo:   2,
			minOrg:    1,
		},
		{
			name: "token authenticated, no org or user (enum)",
			// This configuration currently will only find gists from the user. No repos or orgs will be scanned.
			init: init{
				name: "test source",
				connection: &sourcespb.GitHub{
					Credential: &sourcespb.GitHub_Token{
						Token: githubToken,
					},
				},
			},
			wantChunk: nil,
			wantErr:   false,
			minRepo:   2,
			minOrg:    0,
		},
		{
			name: "unauthenticated, single org",
			init: init{
				name: "test source",
				connection: &sourcespb.GitHub{
					Organizations: []string{"trufflesecurity"},
					Credential:    &sourcespb.GitHub_Unauthenticated{},
					IncludeForks:  true,
				},
			},
			wantChunk: nil,
			wantErr:   false,
			minRepo:   40,
			minOrg:    1,
		},
		{
			name: "unauthenticated, single repo",
			init: init{
				name: "test source",
				connection: &sourcespb.GitHub{
					Repositories: []string{"https://github.com/trufflesecurity/driftwood.git"},
					Credential:   &sourcespb.GitHub_Unauthenticated{},
				},
			},
			wantChunk: &sources.Chunk{
				SourceType: sourcespb.SourceType_SOURCE_TYPE_GITHUB,
				SourceName: "test source",
				SourceMetadata: &source_metadatapb.MetaData{
					Data: &source_metadatapb.MetaData_Github{
						Github: &source_metadatapb.Github{
							Repository: "https://github.com/trufflesecurity/driftwood.git",
						},
					},
				},
				Verify: false,
			},
			wantErr: false,
		},
		{
			name: "app authenticated, no repo or org",
			init: init{
				name: "test source",
				connection: &sourcespb.GitHub{
					ScanUsers: true,
					Credential: &sourcespb.GitHub_GithubApp{
						GithubApp: &credentialspb.GitHubApp{
							PrivateKey:     githubPrivateKeyNew,
							InstallationId: githubInstallationIDNew,
							AppId:          githubAppIDNew,
						},
					},
				},
			},
			wantChunk: nil,
			wantErr:   false,
			minRepo:   32,
			minOrg:    0,
		},
		{
			name: "app authenticated, single repo",
			init: init{
				name: "test source",
				connection: &sourcespb.GitHub{
					Repositories: []string{"https://github.com/truffle-test-integration-org/another-test-repo.git"},
					Credential: &sourcespb.GitHub_GithubApp{
						GithubApp: &credentialspb.GitHubApp{
							PrivateKey:     githubPrivateKeyNew,
							InstallationId: githubInstallationIDNew,
							AppId:          githubAppIDNew,
						},
					},
				},
			},
			wantChunk: &sources.Chunk{
				SourceType: sourcespb.SourceType_SOURCE_TYPE_GITHUB,
				SourceName: "test source",
				SourceMetadata: &source_metadatapb.MetaData{
					Data: &source_metadatapb.MetaData_Github{
						Github: &source_metadatapb.Github{
							Repository: "https://github.com/truffle-test-integration-org/another-test-repo.git",
						},
					},
				},
				Verify: false,
			},
			wantErr: false,
			minRepo: 1,
			minOrg:  0,
		},
		{
			name: "app authenticated, single org",
			init: init{
				name: "test source",
				connection: &sourcespb.GitHub{
					Organizations: []string{"truffle-test-integration-org"},
					Credential: &sourcespb.GitHub_GithubApp{
						GithubApp: &credentialspb.GitHubApp{
							PrivateKey:     githubPrivateKeyNew,
							InstallationId: githubInstallationIDNew,
							AppId:          githubAppIDNew,
						},
					},
				},
			},
			wantChunk: nil,
			wantErr:   false,
			minRepo:   1,
			minOrg:    0,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s := Source{}

			conn, err := anypb.New(tt.init.connection)
			if err != nil {
				t.Fatal(err)
			}

			err = s.Init(ctx, tt.init.name, 0, 0, tt.init.verify, conn, 4)
			if (err != nil) != tt.wantErr {
				t.Errorf("Source.Init() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			chunksCh := make(chan *sources.Chunk, 5)
			go func() {
				err = s.Chunks(ctx, chunksCh)
				if (err != nil) != tt.wantErr {
					if ctx.Err() != nil {
						return
					}
					t.Errorf("Source.Chunks() error = %v, wantErr %v", err, tt.wantErr)
					return
				}
			}()
			if err = sources.HandleTestChannel(chunksCh, basicCheckFunc(tt.minOrg, tt.minRepo, tt.wantChunk, &s)); err != nil {
				t.Error(err)
			}
		})
	}
}

func TestSource_paginateGists(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
	defer cancel()

	type init struct {
		name       string
		verify     bool
		connection *sourcespb.GitHub
	}
	tests := []struct {
		name      string
		init      init
		wantChunk *sources.Chunk
		wantErr   bool
		user      string
		minRepos  int
	}{
		{
			name: "get gist",
			init: init{
				name: "test source",
				connection: &sourcespb.GitHub{
					Credential: &sourcespb.GitHub_Unauthenticated{},
				},
			},
			wantChunk: &sources.Chunk{
				SourceName: "test source",
				SourceMetadata: &source_metadatapb.MetaData{
					Data: &source_metadatapb.MetaData_Github{
						Github: &source_metadatapb.Github{
							Repository: "https://gist.github.com/fecf272c606ddbc5f8486f9c44821312.git",
						},
					},
				},
				Verify: false,
			},
			wantErr:  false,
			user:     "truffle-sandbox",
			minRepos: 1,
		},
		{
			name: "get multiple pages of gists",
			init: init{
				name: "test source",
				connection: &sourcespb.GitHub{
					Credential: &sourcespb.GitHub_Unauthenticated{},
				},
			},
			wantChunk: nil,
			wantErr:   false,
			user:      "andrew",
			minRepos:  101,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s := Source{}

			conn, err := anypb.New(tt.init.connection)
			if err != nil {
				t.Fatal(err)
			}

			err = s.Init(ctx, tt.init.name, 0, 0, tt.init.verify, conn, 4)
			if (err != nil) != tt.wantErr {
				t.Errorf("Source.Init() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			chunksCh := make(chan *sources.Chunk, 5)
			go func() {
				assert.NoError(t, s.addUserGistsToCache(ctx, tt.user, noopReporter()))
				chunksCh <- &sources.Chunk{}
			}()
			var wantedRepo string
			if tt.wantChunk != nil {
				wantedRepo = tt.wantChunk.SourceMetadata.GetGithub().Repository
			}
			if err = sources.HandleTestChannel(chunksCh, gistsCheckFunc(wantedRepo, tt.minRepos, &s)); err != nil {
				t.Error(err)
			}
		})
	}
}

func gistsCheckFunc(expected string, minRepos int, s *Source) sources.ChunkFunc {
	return func(chunk *sources.Chunk) error {
		if minRepos != 0 && minRepos > s.filteredRepoCache.Count() {
			return fmt.Errorf("didn't find enough repos. expected: %d, got :%d", minRepos, len(s.repos))
		}
		if expected != "" {
			for _, repo := range s.filteredRepoCache.Values() {
				if repo == expected {
					return nil
				}
			}
			return fmt.Errorf("expected repo not included: %s", expected)
		}
		return nil
	}
}

func basicCheckFunc(minOrg, minRepo int, wantChunk *sources.Chunk, s *Source) sources.ChunkFunc {
	return func(chunk *sources.Chunk) error {
		if minOrg != 0 && minOrg > s.orgsCache.Count() {
			return fmt.Errorf("incorrect number of orgs. expected at least: %d, got %d", minOrg, s.orgsCache.Count())
		}
		if minRepo != 0 && minRepo > len(s.repos) {
			return fmt.Errorf("incorrect number of repos. expected at least: %d, got %d", minRepo, len(s.repos))
		}
		if wantChunk != nil {
			if diff := pretty.Compare(chunk.SourceMetadata.GetGithub().Repository, wantChunk.SourceMetadata.GetGithub().Repository); diff == "" {
				return nil
			}
			return sources.MatchError
		}
		return nil
	}
}

func githubCommentCheckFunc(gotChunk, wantChunk *sources.Chunk, i int, t *testing.T, name string) {
	if gotChunk.SourceType != wantChunk.SourceType {
		t.Errorf("want SourceType %v, got %v", wantChunk.SourceType, gotChunk.SourceType)
	}

	assert.NotEmpty(t, gotChunk.SourceMetadata.Data, "SourceMetadata.Data should not be empty")

	// First Chunk should be a Issue Comment, Second Chunk should be a PR Comment.
	if i == 1 && name == "token authenticated, single repo, single issue comment" &&
		wantChunk.SourceMetadata.GetGithub().GetLink() != gotChunk.SourceMetadata.GetGithub().GetLink() {
		t.Errorf("want %+v \n got %+v \n", wantChunk.SourceMetadata.GetGithub().GetLink(), gotChunk.SourceMetadata.GetGithub().GetLink())
	} else if i == 2 && name == "token authenticated, single repo, single pr comment" &&
		wantChunk.SourceMetadata.GetGithub().GetLink() != gotChunk.SourceMetadata.GetGithub().GetLink() {
		t.Errorf("want %+v \n got %+v \n", wantChunk.SourceMetadata.GetGithub().GetLink(), gotChunk.SourceMetadata.GetGithub().GetLink())
	}
}

func TestSource_Chunks_TargetedScan(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second*3000)
	defer cancel()

	secret, err := common.GetTestSecret(ctx)
	if err != nil {
		t.Fatal(fmt.Errorf("failed to access secret: %v", err))
	}

	githubToken := secret.MustGetField("GITHUB_TOKEN")

	type init struct {
		name          string
		verify        bool
		connection    *sourcespb.GitHub
		queryCriteria *source_metadatapb.MetaData
	}
	tests := []struct {
		name       string
		init       init
		wantChunks int
		wantErr    bool
	}{
		{
			name: "targeted scan, one file in small commit",
			init: init{
				name:       "test source",
				connection: &sourcespb.GitHub{Credential: &sourcespb.GitHub_Token{Token: githubToken}},
				queryCriteria: &source_metadatapb.MetaData{
					Data: &source_metadatapb.MetaData_Github{
						Github: &source_metadatapb.Github{
							Repository: "test-secrets",
							Link:       "https://github.com/truffle-sandbox/test-secrets/blob/0416560b1330d8ac42045813251d85c688717eaf/new_key#L2",
							Commit:     "0416560b1330d8ac42045813251d85c688717eaf",
							File:       "new_key",
						},
					},
				},
			},
			wantChunks: 1,
		},
		{
			name: "targeted scan, one file in med commit",
			init: init{
				name:       "test source",
				connection: &sourcespb.GitHub{Credential: &sourcespb.GitHub_Unauthenticated{}},
				queryCriteria: &source_metadatapb.MetaData{
					Data: &source_metadatapb.MetaData_Github{
						Github: &source_metadatapb.Github{
							Repository: "https://github.com/trufflesecurity/trufflehog.git",
							Link:       "https://github.com/trufflesecurity/trufflehog/blob/33eed42e17fda8b1a66feaeafcd57efccff26c11/pkg/sources/s3/s3_test.go#L78",
							Commit:     "33eed42e17fda8b1a66feaeafcd57efccff26c11",
							File:       "pkg/sources/s3/s3_test.go",
						},
					},
				},
			},
			wantChunks: 1,
		},
		{
			name: "targeted scan, binary file",
			init: init{
				name:       "test source",
				connection: &sourcespb.GitHub{Credential: &sourcespb.GitHub_Token{Token: githubToken}},
				queryCriteria: &source_metadatapb.MetaData{
					Data: &source_metadatapb.MetaData_Github{
						Github: &source_metadatapb.Github{
							Repository: "https://github.com/truffle-sandbox/test-secrets.git",
							Link:       "https://github.com/truffle-sandbox/test-secrets/blob/70bef8590f87257c0992eecc7db529827a12b801/null_text_w_ptp.ipynb",
							Commit:     "70bef8590f87257c0992eecc7db529827a12b801",
							File:       "null_text_w_ptp.ipynb",
						},
					},
				},
			},
			wantChunks: 607,
		},
		{
			name: "targeted scan, commit metadata",
			init: init{
				name:       "test source",
				connection: &sourcespb.GitHub{Credential: &sourcespb.GitHub_Token{Token: githubToken}},
				queryCriteria: &source_metadatapb.MetaData{
					Data: &source_metadatapb.MetaData_Github{
						Github: &source_metadatapb.Github{
							Repository: "https://github.com/trufflesecurity/trufflehog.git",
							Link:       "https://github.com/trufflesecurity/trufflehog/commit/1c51106e35c3b3c327fe12e358177c03079bb771",
							Commit:     "1c51106e35c3b3c327fe12e358177c03079bb771",
							File:       "", // no file
						},
					},
				},
			},
			wantChunks: 1,
		},
		{
			name: "no file in commit",
			init: init{
				name:       "test source",
				connection: &sourcespb.GitHub{Credential: &sourcespb.GitHub_Unauthenticated{}},
				queryCriteria: &source_metadatapb.MetaData{
					Data: &source_metadatapb.MetaData_Github{
						Github: &source_metadatapb.Github{
							Repository: "test_keys",
							Link:       "https://github.com/trufflesecurity/test_keys/blob/fbc14303ffbf8fb1c2c1914e8dda7d0121633aca/keys#L4",
							Commit:     "fbc14303ffbf8fb1c2c1914e8dda7d0121633aca",
							File:       "not-the-file",
						},
					},
				},
			},
			wantChunks: 0,
			wantErr:    true,
		},
		{
			name: "invalid query criteria, malformed link",
			init: init{
				name:       "test source",
				connection: &sourcespb.GitHub{Credential: &sourcespb.GitHub_Unauthenticated{}},
				queryCriteria: &source_metadatapb.MetaData{
					Data: &source_metadatapb.MetaData_Github{
						Github: &source_metadatapb.Github{
							Repository: "test_keys",
							Link:       "malformed-link",
							Commit:     "fbc14303ffbf8fb1c2c1914e8dda7d0121633aca",
							File:       "not-the-file",
						},
					},
				},
			},
			wantChunks: 0,
			wantErr:    true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s := Source{}

			conn, err := anypb.New(tt.init.connection)
			assert.Nil(t, err)

			err = s.Init(ctx, tt.init.name, 0, 0, tt.init.verify, conn, 8)
			assert.Nil(t, err)

			chunksCh := make(chan *sources.Chunk, 1)
			go func() {
				defer close(chunksCh)
				err = s.Chunks(ctx, chunksCh, sources.ChunkingTarget{QueryCriteria: tt.init.queryCriteria})
				if tt.wantErr {
					assert.Error(t, err)
				} else {
					assert.NoError(t, err)
				}
			}()

			i := 0
			for range chunksCh {
				i++
			}
			assert.Equal(t, tt.wantChunks, i)
		})
	}
}

func TestChunkUnit(t *testing.T) {
	ctx := context.Background()
	conn, _ := anypb.New(&sourcespb.GitHub{
		Repositories: []string{"https://github.com/trufflesecurity/driftwood.git"},
		Credential:   &sourcespb.GitHub_Unauthenticated{},
	})
	s := Source{}
	if err := s.Init(ctx, "github integration test source", 0, 0, false, conn, 1); err != nil {
		t.Errorf("Init() failed: %v", err)
	}

	unit := RepoUnit{Name: "driftwood", URL: "https://github.com/trufflesecurity/driftwood.git"}
	reporter := &countChunkReporter{}
	if err := s.ChunkUnit(ctx, unit, reporter); err != nil {
		t.Errorf("ChunkUnit() failed: %v", err)
	}
	assert.GreaterOrEqual(t, reporter.chunkCount, 65)
	assert.Equal(t, 0, reporter.errCount)
}

func TestSource_Validate(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
	defer cancel()

	secret, err := common.GetTestSecret(ctx)
	if err != nil {
		t.Fatal(fmt.Errorf("failed to access secret: %v", err))
	}

	githubToken := secret.MustGetField("GITHUB_TOKEN")
	githubPrivateKeyB64New := secret.MustGetField("GITHUB_PRIVATE_KEY_NEW")
	githubInstallationIDNew := secret.MustGetField("GITHUB_INSTALLATION_ID_NEW")
	githubAppIDNew := secret.MustGetField("GITHUB_APP_ID_NEW")

	githubPrivateKeyBytesNew, err := base64.StdEncoding.DecodeString(githubPrivateKeyB64New)
	if err != nil {
		t.Fatal(err)
	}
	githubPrivateKeyNew := string(githubPrivateKeyBytesNew)

	type args struct {
		ctx context.Context
	}
	tests := []struct {
		name         string
		args         args
		sourceConfig *Source
		wantErr      bool
	}{
		{
			name:         "success - validate - unauthenticated",
			args:         args{ctx: context.Background()},
			sourceConfig: &Source{conn: &sourcespb.GitHub{Credential: &sourcespb.GitHub_Unauthenticated{}}},
			wantErr:      false,
		},
		{
			name:         "success - validate - token authentication",
			args:         args{ctx: context.Background()},
			sourceConfig: &Source{conn: &sourcespb.GitHub{Credential: &sourcespb.GitHub_Token{Token: githubToken}}},
			wantErr:      false,
		},
		{
			name: "sucess- validate - app token authentication",
			args: args{ctx: context.Background()},
			sourceConfig: &Source{conn: &sourcespb.GitHub{Credential: &sourcespb.GitHub_GithubApp{
				GithubApp: &credentialspb.GitHubApp{
					PrivateKey:     githubPrivateKeyNew,
					InstallationId: githubInstallationIDNew,
					AppId:          githubAppIDNew,
				},
			}}},
			wantErr: false,
		},
		{
			name:         "fail - validate - token authentication",
			args:         args{ctx: context.Background()},
			sourceConfig: &Source{conn: &sourcespb.GitHub{Credential: &sourcespb.GitHub_Token{Token: githubToken + "fake"}}},
			wantErr:      true,
		},
		{
			name: "fail- validate - app token authentication",
			args: args{ctx: context.Background()},
			sourceConfig: &Source{conn: &sourcespb.GitHub{Credential: &sourcespb.GitHub_GithubApp{
				GithubApp: &credentialspb.GitHubApp{
					PrivateKey:     githubPrivateKeyNew + "fake",
					InstallationId: githubInstallationIDNew + "0",
					AppId:          githubAppIDNew,
				},
			}}},
			wantErr: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			connector, err := newConnector(ctx, tt.sourceConfig)
			require.NoError(t, err)
			tt.sourceConfig.connector = connector

			if err := tt.sourceConfig.Validate(tt.args.ctx); err != nil && !tt.wantErr {
				t.Errorf("Source.Validate() = %v, wantErr %t", err, tt.wantErr)
			}
		})
	}
}

func TestSource_ScanCommentsWithGraphql(t *testing.T) {
	feature.UseGithubGraphQLAPI.Store(true)

	ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
	defer cancel()

	source := &sourcespb.GitHub{
		Repositories:               []string{"https://github.com/trufflesecurity/driftwood.git"},
		IncludeIssueComments:       true,
		IncludePullRequestComments: true,
		Credential:                 &sourcespb.GitHub_Unauthenticated{},
	}

	wantChunk := sources.Chunk{
		SourceType: sourcespb.SourceType_SOURCE_TYPE_GITHUB,
		SourceName: "test source",
		SourceMetadata: &source_metadatapb.MetaData{
			Data: &source_metadatapb.MetaData_Github{
				Github: &source_metadatapb.Github{
					Link:      "https://github.com/trufflesecurity/driftwood.git/issues/1",
					Username:  "truffle-sandbox",
					Timestamp: "2023-06-22 23:33:46 +0000 UTC",
				},
			},
		},
		Verify: false,
	}

	s := Source{}

	conn, err := anypb.New(source)
	assert.NoError(t, err)

	err = s.Init(ctx, "test-source", 0, 0, false, conn, 4)
	assert.NoError(t, err)

	chunksCh := make(chan *sources.Chunk, 1)
	go func() {
		// Close the channel
		defer close(chunksCh)
		err = s.Chunks(ctx, chunksCh)
		assert.NoError(t, err)
	}()

	i := 0
	for gotChunk := range chunksCh {
		// Skip chunks that are not comments.
		if gotChunk.SourceMetadata.GetGithub().GetCommit() != "" {
			continue
		}
		i++
		githubCommentCheckFunc(gotChunk, &wantChunk, i, t, "test-source")
	}

	// Confirm all comments were processed.
	assert.Equal(t, i, 5)
}

type countChunkReporter struct {
	chunkCount int
	errCount   int
}

func (m *countChunkReporter) ChunkOk(ctx context.Context, chunk sources.Chunk) error {
	m.chunkCount++
	return nil
}

func (m *countChunkReporter) ChunkErr(ctx context.Context, err error) error {
	m.errCount++
	return nil
}
