package secret_test

import (
	"bytes"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/aquasecurity/trivy/pkg/fanal/secret"
	"github.com/aquasecurity/trivy/pkg/fanal/types"
	"github.com/aquasecurity/trivy/pkg/log"
)

func TestMain(m *testing.M) {
	log.InitLogger(false, true)
	os.Exit(m.Run())
}

func TestSecretScanner(t *testing.T) {
	wantFinding1 := types.SecretFinding{
		RuleID:    "rule1",
		Category:  "general",
		Title:     "Generic Rule",
		Severity:  "HIGH",
		StartLine: 2,
		EndLine:   2,
		Match:     "generic secret line secret=\"*********\"",
		Code: types.Code{
			Lines: []types.Line{
				{
					Number:      1,
					Content:     "--- ignore block start ---",
					Highlighted: "--- ignore block start ---",
				},
				{
					Number:      2,
					Content:     "generic secret line secret=\"*********\"",
					Highlighted: "generic secret line secret=\"*********\"",
					IsCause:     true,
					FirstCause:  true,
					LastCause:   true,
				},
				{
					Number:      3,
					Content:     "--- ignore block stop ---",
					Highlighted: "--- ignore block stop ---",
				},
			},
		},
		Offset: 55,
	}
	wantFinding2 := types.SecretFinding{
		RuleID:    "rule1",
		Category:  "general",
		Title:     "Generic Rule",
		Severity:  "HIGH",
		StartLine: 4,
		EndLine:   4,
		Match:     "secret=\"**********\"",
		Code: types.Code{
			Lines: []types.Line{
				{
					Number:      2,
					Content:     "generic secret line secret=\"*********\"",
					Highlighted: "generic secret line secret=\"*********\"",
				},
				{
					Number:      3,
					Content:     "--- ignore block stop ---",
					Highlighted: "--- ignore block stop ---",
				},
				{
					Number:      4,
					Content:     "secret=\"**********\"",
					Highlighted: "secret=\"**********\"",
					IsCause:     true,
					FirstCause:  true,
					LastCause:   true,
				},
				{
					Number:      5,
					Content:     "credentials: { user: \"username\" password: \"123456789\" }",
					Highlighted: "credentials: { user: \"username\" password: \"123456789\" }",
				},
			},
		},
		Offset: 100,
	}
	wantFindingRegexDisabled := types.SecretFinding{
		RuleID:    "rule1",
		Category:  "general",
		Title:     "Generic Rule",
		Severity:  "HIGH",
		StartLine: 4,
		EndLine:   4,
		Match:     "secret=\"**********\"",
		Code: types.Code{
			Lines: []types.Line{
				{
					Number:      2,
					Content:     "generic secret line secret=\"somevalue\"",
					Highlighted: "generic secret line secret=\"somevalue\"",
				},
				{
					Number:      3,
					Content:     "--- ignore block stop ---",
					Highlighted: "--- ignore block stop ---",
				},
				{
					Number:      4,
					Content:     "secret=\"**********\"",
					Highlighted: "secret=\"**********\"",
					IsCause:     true,
					FirstCause:  true,
					LastCause:   true,
				},
				{
					Number:      5,
					Content:     "credentials: { user: \"username\" password: \"123456789\" }",
					Highlighted: "credentials: { user: \"username\" password: \"123456789\" }",
				},
			},
		},
		Offset: 100,
	}
	wantFindingMultipleGroupsUsername := types.SecretFinding{
		RuleID:    "rule1",
		Category:  "general",
		Title:     "Generic Rule",
		Severity:  "HIGH",
		StartLine: 5,
		EndLine:   5,
		Match:     "credentials: { user: \"********\" password: \"*********\" }",
		Code: types.Code{
			Lines: []types.Line{
				{
					Number:      3,
					Content:     "--- ignore block stop ---",
					Highlighted: "--- ignore block stop ---",
				},
				{
					Number:      4,
					Content:     "secret=\"othervalue\"",
					Highlighted: "secret=\"othervalue\"",
				},
				{
					Number:      5,
					Content:     "credentials: { user: \"********\" password: \"*********\" }",
					Highlighted: "credentials: { user: \"********\" password: \"*********\" }",
					IsCause:     true,
					FirstCause:  true,
					LastCause:   true,
				},
			},
		},
		Offset: 134,
	}
	wantFindingMultipleGroupsPassword := types.SecretFinding{
		RuleID:    "rule1",
		Category:  "general",
		Title:     "Generic Rule",
		Severity:  "HIGH",
		StartLine: 5,
		EndLine:   5,
		Match:     "credentials: { user: \"********\" password: \"*********\" }",
		Code: types.Code{
			Lines: []types.Line{
				{
					Number:      3,
					Content:     "--- ignore block stop ---",
					Highlighted: "--- ignore block stop ---",
				},
				{
					Number:      4,
					Content:     "secret=\"othervalue\"",
					Highlighted: "secret=\"othervalue\"",
				},
				{
					Number:      5,
					Content:     "credentials: { user: \"********\" password: \"*********\" }",
					Highlighted: "credentials: { user: \"********\" password: \"*********\" }",
					IsCause:     true,
					FirstCause:  true,
					LastCause:   true,
				},
			},
		},
		Offset: 155,
	}
	wantFinding5 := types.SecretFinding{
		RuleID:    "aws-access-key-id",
		Category:  secret.CategoryAWS,
		Title:     "AWS Access Key ID",
		Severity:  "CRITICAL",
		StartLine: 2,
		EndLine:   2,
		Match:     "AWS_ACCESS_KEY_ID=********************",
		Code: types.Code{
			Lines: []types.Line{
				{
					Number:      1,
					Content:     "'AWS_secret_KEY'=\"****************************************\"",
					Highlighted: "'AWS_secret_KEY'=\"****************************************\"",
				},
				{
					Number:      2,
					Content:     "AWS_ACCESS_KEY_ID=********************",
					Highlighted: "AWS_ACCESS_KEY_ID=********************",
					IsCause:     true,
					FirstCause:  true,
					LastCause:   true,
				},
				{
					Number:      3,
					Content:     "\"aws_account_ID\":'1234-5678-9123'",
					Highlighted: "\"aws_account_ID\":'1234-5678-9123'",
				},
			},
		},
		Offset: 78,
	}
	wantFinding5a := types.SecretFinding{
		RuleID:    "aws-access-key-id",
		Category:  secret.CategoryAWS,
		Title:     "AWS Access Key ID",
		Severity:  "CRITICAL",
		StartLine: 2,
		EndLine:   2,
		Match:     "AWS_ACCESS_KEY_ID=********************",
		Code: types.Code{
			Lines: []types.Line{
				{
					Number:      1,
					Content:     "GITHUB_PAT=****************************************",
					Highlighted: "GITHUB_PAT=****************************************",
				},
				{
					Number:      2,
					Content:     "AWS_ACCESS_KEY_ID=********************",
					Highlighted: "AWS_ACCESS_KEY_ID=********************",
					IsCause:     true,
					FirstCause:  true,
					LastCause:   true,
				},
			},
		},
		Offset: 70,
	}
	wantFindingPATDisabled := types.SecretFinding{
		RuleID:    "aws-access-key-id",
		Category:  secret.CategoryAWS,
		Title:     "AWS Access Key ID",
		Severity:  "CRITICAL",
		StartLine: 2,
		EndLine:   2,
		Match:     "AWS_ACCESS_KEY_ID=********************",
		Code: types.Code{
			Lines: []types.Line{
				{
					Number:      1,
					Content:     "GITHUB_PAT=ghp_012345678901234567890123456789abcdef",
					Highlighted: "GITHUB_PAT=ghp_012345678901234567890123456789abcdef",
				},
				{
					Number:      2,
					Content:     "AWS_ACCESS_KEY_ID=********************",
					Highlighted: "AWS_ACCESS_KEY_ID=********************",
					IsCause:     true,
					FirstCause:  true,
					LastCause:   true,
				},
			},
		},
		Offset: 70,
	}
	wantFinding6 := types.SecretFinding{
		RuleID:    "github-pat",
		Category:  secret.CategoryGitHub,
		Title:     "GitHub Personal Access Token",
		Severity:  "CRITICAL",
		StartLine: 1,
		EndLine:   1,
		Match:     "GITHUB_PAT=****************************************",
		Code: types.Code{
			Lines: []types.Line{
				{
					Number:      1,
					Content:     "GITHUB_PAT=****************************************",
					Highlighted: "GITHUB_PAT=****************************************",
					IsCause:     true,
					FirstCause:  true,
					LastCause:   true,
				},
				{
					Number:      2,
					Content:     "AWS_ACCESS_KEY_ID=********************",
					Highlighted: "AWS_ACCESS_KEY_ID=********************",
				},
			},
		},
		Offset: 11,
	}
	wantFindingGitHubPAT := types.SecretFinding{
		RuleID:    "github-fine-grained-pat",
		Category:  secret.CategoryGitHub,
		Title:     "GitHub Fine-grained personal access tokens",
		Severity:  "CRITICAL",
		StartLine: 1,
		EndLine:   1,
		Match:     "GITHUB_TOKEN=*********************************************************************************************",
		Code: types.Code{
			Lines: []types.Line{
				{
					Number:      1,
					Content:     "GITHUB_TOKEN=*********************************************************************************************",
					Highlighted: "GITHUB_TOKEN=*********************************************************************************************",
					IsCause:     true,
					FirstCause:  true,
					LastCause:   true,
				},
			},
		},
		Offset: 13,
	}
	wantFindingMyAwsAccessKey := types.SecretFinding{
		RuleID:    "aws-secret-access-key",
		Category:  secret.CategoryAWS,
		Title:     "AWS Secret Access Key",
		Severity:  "CRITICAL",
		StartLine: 1,
		EndLine:   1,
		Match:     `MyAWS_secret_KEY="****************************************"`,
		Code: types.Code{
			Lines: []types.Line{
				{
					Number:      1,
					Content:     "MyAWS_secret_KEY=\"****************************************\"",
					Highlighted: "MyAWS_secret_KEY=\"****************************************\"",
					IsCause:     true,
					FirstCause:  true,
					LastCause:   true,
				},
				{
					Number:      2,
					Content:     "our*********************************************************************************************",
					Highlighted: "our*********************************************************************************************",
				},
			},
		},
		Offset: 18,
	}
	wantFindingMyGitHubPAT := types.SecretFinding{
		RuleID:    "github-fine-grained-pat",
		Category:  secret.CategoryGitHub,
		Title:     "GitHub Fine-grained personal access tokens",
		Severity:  "CRITICAL",
		StartLine: 2,
		EndLine:   2,
		Match:     "our*********************************************************************************************",
		Code: types.Code{
			Lines: []types.Line{
				{
					Number:      1,
					Content:     "MyAWS_secret_KEY=\"****************************************\"",
					Highlighted: "MyAWS_secret_KEY=\"****************************************\"",
				},
				{
					Number:      2,
					Content:     "our*********************************************************************************************",
					Highlighted: "our*********************************************************************************************",
					IsCause:     true,
					FirstCause:  true,
					LastCause:   true,
				},
			},
		},
		Offset: 63,
	}
	wantFindingGHButDisableAWS := types.SecretFinding{
		RuleID:    "github-pat",
		Category:  secret.CategoryGitHub,
		Title:     "GitHub Personal Access Token",
		Severity:  "CRITICAL",
		StartLine: 1,
		EndLine:   1,
		Match:     "GITHUB_PAT=****************************************",
		Code: types.Code{
			Lines: []types.Line{
				{
					Number:      1,
					Content:     "GITHUB_PAT=****************************************",
					Highlighted: "GITHUB_PAT=****************************************",
					IsCause:     true,
					FirstCause:  true,
					LastCause:   true,
				},
				{
					Number:      2,
					Content:     "AWS_ACCESS_KEY_ID=AKIA0123456789ABCDEF",
					Highlighted: "AWS_ACCESS_KEY_ID=AKIA0123456789ABCDEF",
				},
			},
		},
		Offset: 11,
	}
	wantFinding7 := types.SecretFinding{
		RuleID:    "github-pat",
		Category:  secret.CategoryGitHub,
		Title:     "GitHub Personal Access Token",
		Severity:  "CRITICAL",
		StartLine: 1,
		EndLine:   1,
		Match:     "aaaaaaaaaaaaaaaaaa GITHUB_PAT=**************************************** bbbbbbbbbbbbbbbbbbb",
		Code: types.Code{
			Lines: []types.Line{
				{
					Number:      1,
					Content:     "aaaaaaaaaaaaaaaaaa GITHUB_PAT=**************************************** bbbbbbbbbbbbbbbbbbb",
					Highlighted: "aaaaaaaaaaaaaaaaaa GITHUB_PAT=**************************************** bbbbbbbbbbbbbbbbbbb",
					IsCause:     true,
					FirstCause:  true,
					LastCause:   true,
				},
			},
		},
		Offset: 67,
	}
	wantFinding8 := types.SecretFinding{
		RuleID:    "rule1",
		Category:  "general",
		Title:     "Generic Rule",
		Severity:  "UNKNOWN",
		StartLine: 2,
		EndLine:   2,
		Match:     "generic secret line secret=\"*********\"",
		Code: types.Code{
			Lines: []types.Line{
				{
					Number:      1,
					Content:     "--- ignore block start ---",
					Highlighted: "--- ignore block start ---",
				},
				{
					Number:      2,
					Content:     "generic secret line secret=\"*********\"",
					Highlighted: "generic secret line secret=\"*********\"",
					IsCause:     true,
					FirstCause:  true,
					LastCause:   true,
				},
				{
					Number:      3,
					Content:     "--- ignore block stop ---",
					Highlighted: "--- ignore block stop ---",
				},
			},
		},
		Offset: 55,
	}
	wantFinding9 := types.SecretFinding{
		RuleID:    "aws-secret-access-key",
		Category:  secret.CategoryAWS,
		Title:     "AWS Secret Access Key",
		Severity:  "CRITICAL",
		StartLine: 1,
		EndLine:   1,
		Match:     `'AWS_secret_KEY'="****************************************"`,
		Code: types.Code{
			Lines: []types.Line{
				{
					Number:      1,
					Content:     "'AWS_secret_KEY'=\"****************************************\"",
					Highlighted: "'AWS_secret_KEY'=\"****************************************\"",
					IsCause:     true,
					FirstCause:  true,
					LastCause:   true,
				},
				{
					Number:      2,
					Content:     "AWS_ACCESS_KEY_ID=********************",
					Highlighted: "AWS_ACCESS_KEY_ID=********************",
				},
			},
		},
		Offset: 18,
	}
	wantFinding10 := types.SecretFinding{
		RuleID:    "aws-secret-access-key",
		Category:  secret.CategoryAWS,
		Title:     "AWS Secret Access Key",
		Severity:  "CRITICAL",
		StartLine: 5,
		EndLine:   5,
		Match:     `  "created_by": "ENV aws_sec_key "****************************************",`,
		Code: types.Code{
			Lines: []types.Line{
				{
					Number:      3,
					Content:     "\"aws_account_ID\":'1234-5678-9123'",
					Highlighted: "\"aws_account_ID\":'1234-5678-9123'",
				},
				{
					Number:      4,
					Content:     "AWS_example=AKIAIOSFODNN7EXAMPLE",
					Highlighted: "AWS_example=AKIAIOSFODNN7EXAMPLE",
				},
				{
					Number:      5,
					Content:     "  \"created_by\": \"ENV aws_sec_key \"****************************************\",",
					Highlighted: "  \"created_by\": \"ENV aws_sec_key \"****************************************\",",
					IsCause:     true,
					FirstCause:  true,
					LastCause:   true,
				},
			},
		},
		Offset: 200,
	}
	wantFindingAsymmetricPrivateKeyJson := types.SecretFinding{
		RuleID:    "private-key",
		Category:  secret.CategoryAsymmetricPrivateKey,
		Title:     "Asymmetric Private Key",
		Severity:  "HIGH",
		StartLine: 1,
		EndLine:   1,
		Match:     "----BEGIN RSA PRIVATE KEY-----**************************************************************************************************************************-----END RSA PRIVATE",
		Code: types.Code{
			Lines: []types.Line{
				{
					Number:      1,
					Content:     "----BEGIN RSA PRIVATE KEY-----**************************************************************************************************************************-----END RSA PRIVATE",
					Highlighted: "----BEGIN RSA PRIVATE KEY-----**************************************************************************************************************************-----END RSA PRIVATE",
					IsCause:     true,
					FirstCause:  true,
					LastCause:   true,
				},
			},
		},
		Offset: 40,
	}
	wantFindingAsymmetricPrivateKey := types.SecretFinding{
		RuleID:    "private-key",
		Category:  secret.CategoryAsymmetricPrivateKey,
		Title:     "Asymmetric Private Key",
		Severity:  "HIGH",
		StartLine: 2,
		EndLine:   4,
		Match:     "****************************************************************",
		Code: types.Code{
			Lines: []types.Line{
				{
					Number:      1,
					Content:     "-----BEGIN RSA PRIVATE KEY-----",
					Highlighted: "-----BEGIN RSA PRIVATE KEY-----",
				},
				{
					Number:      2,
					Content:     "****************************************************************",
					Highlighted: "****************************************************************",
					IsCause:     true,
					FirstCause:  true,
					LastCause:   false,
				},
				{
					Number:      3,
					Content:     "****************************************************************",
					Highlighted: "****************************************************************",
					IsCause:     true,
					FirstCause:  false,
					LastCause:   false,
				},
				{
					Number:      4,
					Content:     "****************************************************",
					Highlighted: "****************************************************",
					IsCause:     true,
					FirstCause:  false,
					LastCause:   true,
				},
				{
					Number:      5,
					Content:     "-----END RSA PRIVATE KEY-----",
					Highlighted: "-----END RSA PRIVATE KEY-----",
				},
			},
		},
		Offset: 32,
	}
	wantFindingAsymmSecretKey := types.SecretFinding{
		RuleID:    "private-key",
		Category:  secret.CategoryAsymmetricPrivateKey,
		Title:     "Asymmetric Private Key",
		Severity:  "HIGH",
		StartLine: 2,
		EndLine:   3,
		Match:     "***************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************",
		Code: types.Code{
			Lines: []types.Line{
				{
					Number:      1,
					Content:     "-----BEGIN RSA PRIVATE KEY-----",
					Highlighted: "-----BEGIN RSA PRIVATE KEY-----",
					IsCause:     false,
					FirstCause:  false,
					LastCause:   false,
				},
				{
					Number:      2,
					Content:     "***************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************",
					Highlighted: "***************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************",
					IsCause:     true,
					FirstCause:  true,
					LastCause:   false,
				},
				{
					Number:      3,
					Content:     "********************************************************************",
					Highlighted: "********************************************************************",
					IsCause:     true,
					FirstCause:  false,
					LastCause:   true,
				},
				{
					Number:      4,
					Content:     "-----END RSA PRIVATE KEY-----",
					Highlighted: "-----END RSA PRIVATE KEY-----",
					IsCause:     false,
					FirstCause:  false,
					LastCause:   false,
				},
			},
		},
		Offset: 32,
	}
	wantFindingMinimumAsymmSecretKey := types.SecretFinding{
		RuleID:    "private-key",
		Category:  secret.CategoryAsymmetricPrivateKey,
		Title:     "Asymmetric Private Key",
		Severity:  "HIGH",
		StartLine: 9,
		EndLine:   9,
		Match:     "-----BEGIN PRIVATE KEY-----********************************-----END PRIVATE KEY-----",
		Code: types.Code{
			Lines: []types.Line{
				{
					Number:      7,
					Content:     "",
					Highlighted: "",
					IsCause:     false,
					FirstCause:  false,
					LastCause:   false,
				},
				{
					Number:      8,
					Content:     "## Theoretically Asymmetric Private Key of minimum length (RSA 128 bits)",
					Highlighted: "## Theoretically Asymmetric Private Key of minimum length (RSA 128 bits)",
					IsCause:     false,
					FirstCause:  false,
					LastCause:   false,
				},
				{
					Number:      9,
					Content:     "-----BEGIN PRIVATE KEY-----********************************-----END PRIVATE KEY-----",
					Highlighted: "-----BEGIN PRIVATE KEY-----********************************-----END PRIVATE KEY-----",
					IsCause:     true,
					FirstCause:  true,
					LastCause:   true,
				},
			},
		},
		Offset: 1842,
	}
	wantFindingAlibabaAccessKeyId := types.SecretFinding{
		RuleID:    "alibaba-access-key-id",
		Category:  secret.CategoryAlibaba,
		Title:     "Alibaba AccessKey ID",
		Severity:  "HIGH",
		StartLine: 2,
		EndLine:   2,
		Match:     "key = ************************,",
		Code: types.Code{
			Lines: []types.Line{
				{
					Number:      1,
					Content:     "key : LTAI1234567890ABCDEFG123asd",
					Highlighted: "key : LTAI1234567890ABCDEFG123asd",
				},
				{
					Number:      2,
					Content:     "key = ************************,",
					Highlighted: "key = ************************,",
					IsCause:     true,
					FirstCause:  true,
					LastCause:   true,
				},
				{
					Number:      3,
					Content:     "asdLTAI1234567890ABCDEFG123",
					Highlighted: "asdLTAI1234567890ABCDEFG123",
				},
			},
		},
		Offset: 40,
	}
	wantFindingDockerKey1 := types.SecretFinding{
		RuleID:    "dockerconfig-secret",
		Category:  secret.CategoryDocker,
		Title:     "Dockerconfig secret exposed",
		Severity:  "HIGH",
		StartLine: 4,
		EndLine:   4,
		Match:     "  .dockercfg: ************",
		Code: types.Code{
			Lines: []types.Line{
				{
					Number:      2,
					Content:     "  .dockerconfigjson: ************",
					Highlighted: "  .dockerconfigjson: ************",
				},
				{
					Number:      3,
					Content:     "data2:",
					Highlighted: "data2:",
				},
				{
					Number:      4,
					Content:     "  .dockercfg: ************",
					Highlighted: "  .dockercfg: ************",
					IsCause:     true,
					FirstCause:  true,
					LastCause:   true,
				},
			},
		},
		Offset: 62,
	}
	wantFindingDockerKey2 := types.SecretFinding{
		RuleID:    "dockerconfig-secret",
		Category:  secret.CategoryDocker,
		Title:     "Dockerconfig secret exposed",
		Severity:  "HIGH",
		StartLine: 2,
		EndLine:   2,
		Match:     "  .dockerconfigjson: ************",
		Code: types.Code{
			Lines: []types.Line{
				{
					Number:      1,
					Content:     "data1:",
					Highlighted: "data1:",
				},
				{
					Number:      2,
					Content:     "  .dockerconfigjson: ************",
					Highlighted: "  .dockerconfigjson: ************",
					IsCause:     true,
					FirstCause:  true,
					LastCause:   true,
				},
				{
					Number:      3,
					Content:     "data2:",
					Highlighted: "data2:",
				},
			},
		},
		Offset: 28,
	}
	wantFindingPrivatePackagistOrgReadToken := types.SecretFinding{
		RuleID:    "private-packagist-token",
		Category:  secret.CategoryPrivatePackagist,
		Title:     "Private Packagist token",
		Severity:  "HIGH",
		StartLine: 1,
		EndLine:   1,
		Match:     "ORG_READ_TOKEN=**********************************************************************************",
		Code: types.Code{
			Lines: []types.Line{
				{
					Number:      1,
					Content:     "ORG_READ_TOKEN=**********************************************************************************",
					Highlighted: "ORG_READ_TOKEN=**********************************************************************************",
					IsCause:     true,
					FirstCause:  true,
					LastCause:   true,
				},
				{
					Number:      2,
					Content:     "ORG_WRITE_TOKEN=**********************************************************************************",
					Highlighted: "ORG_WRITE_TOKEN=**********************************************************************************",
					IsCause:     false,
					FirstCause:  false,
					LastCause:   false,
				},
			},
		},
		Offset: 15,
	}
	wantFindingPrivatePackagistOrgUpdateToken := types.SecretFinding{
		RuleID:    "private-packagist-token",
		Category:  secret.CategoryPrivatePackagist,
		Title:     "Private Packagist token",
		Severity:  "HIGH",
		StartLine: 2,
		EndLine:   2,
		Match:     "ORG_WRITE_TOKEN=**********************************************************************************",
		Code: types.Code{
			Lines: []types.Line{
				{
					Number:      1,
					Content:     "ORG_READ_TOKEN=**********************************************************************************",
					Highlighted: "ORG_READ_TOKEN=**********************************************************************************",
					IsCause:     false,
					FirstCause:  false,
					LastCause:   false,
				},
				{
					Number:      2,
					Content:     "ORG_WRITE_TOKEN=**********************************************************************************",
					Highlighted: "ORG_WRITE_TOKEN=**********************************************************************************",
					IsCause:     true,
					FirstCause:  true,
					LastCause:   true,
				},
				{
					Number:      3,
					Content:     "USER_TOKEN=**********************************************************************************",
					Highlighted: "USER_TOKEN=**********************************************************************************",
					IsCause:     false,
					FirstCause:  false,
					LastCause:   false,
				},
			},
		},
		Offset: 114,
	}
	wantFindingPrivatePackagistUserToken := types.SecretFinding{
		RuleID:    "private-packagist-token",
		Category:  secret.CategoryPrivatePackagist,
		Title:     "Private Packagist token",
		Severity:  "HIGH",
		StartLine: 3,
		EndLine:   3,
		Match:     "USER_TOKEN=**********************************************************************************",
		Code: types.Code{
			Lines: []types.Line{
				{
					Number:      1,
					Content:     "ORG_READ_TOKEN=**********************************************************************************",
					Highlighted: "ORG_READ_TOKEN=**********************************************************************************",
					IsCause:     false,
					FirstCause:  false,
					LastCause:   false,
				},
				{
					Number:      2,
					Content:     "ORG_WRITE_TOKEN=**********************************************************************************",
					Highlighted: "ORG_WRITE_TOKEN=**********************************************************************************",
					IsCause:     false,
					FirstCause:  false,
					LastCause:   false,
				},
				{
					Number:      3,
					Content:     "USER_TOKEN=**********************************************************************************",
					Highlighted: "USER_TOKEN=**********************************************************************************",
					IsCause:     true,
					FirstCause:  true,
					LastCause:   true,
				},
				{
					Number:      4,
					Content:     "",
					Highlighted: "",
					IsCause:     false,
					FirstCause:  false,
					LastCause:   false,
				},
			},
		},
		Offset: 208,
	}
	wantFindingHuggingFace := types.SecretFinding{
		RuleID:    "hugging-face-access-token",
		Category:  secret.CategoryHuggingFace,
		Title:     "Hugging Face Access Token",
		Severity:  "CRITICAL",
		StartLine: 1,
		EndLine:   1,
		Match:     "HF_example_token: ******************************************",
		Code: types.Code{
			Lines: []types.Line{
				{
					Number:      1,
					Content:     "HF_example_token: ******************************************",
					Highlighted: "HF_example_token: ******************************************",
					IsCause:     true,
					FirstCause:  true,
					LastCause:   true,
				},
				{
					Number:      2,
					Content:     "",
					Highlighted: "",
					IsCause:     false,
					FirstCause:  false,
					LastCause:   false,
				},
			},
		},
		Offset: 18,
	}
	wantFindingGrafanaQuoted := types.SecretFinding{
		RuleID:    "grafana-api-token",
		Category:  secret.CategoryGrafana,
		Title:     "Grafana API token",
		Severity:  "MEDIUM",
		StartLine: 1,
		EndLine:   1,
		Match:     "GRAFANA_TOKEN=**********************************************************************************************",
		Code: types.Code{
			Lines: []types.Line{
				{
					Number:      1,
					Content:     "GRAFANA_TOKEN=**********************************************************************************************",
					Highlighted: "GRAFANA_TOKEN=**********************************************************************************************",
					IsCause:     true,
					FirstCause:  true,
					LastCause:   true,
				},
				{
					Number:      2,
					Content:     "GRAFANA_TOKEN=**************************************************************************************",
					Highlighted: "GRAFANA_TOKEN=**************************************************************************************",
					IsCause:     false,
					FirstCause:  false,
					LastCause:   false,
				},
			},
		},
		Offset: 14,
	}
	wantFindingGrafanaUnquoted := types.SecretFinding{
		RuleID:    "grafana-api-token",
		Category:  secret.CategoryGrafana,
		Title:     "Grafana API token",
		Severity:  "MEDIUM",
		StartLine: 2,
		EndLine:   2,
		Match:     "GRAFANA_TOKEN=********************************************************************************************",
		Code: types.Code{
			Lines: []types.Line{
				{
					Number:      1,
					Content:     "GRAFANA_TOKEN=**************************************************************************************",
					Highlighted: "GRAFANA_TOKEN=**************************************************************************************",
					IsCause:     false,
					FirstCause:  false,
					LastCause:   false,
				},
				{
					Number:      2,
					Content:     "GRAFANA_TOKEN=********************************************************************************************",
					Highlighted: "GRAFANA_TOKEN=********************************************************************************************",
					IsCause:     true,
					FirstCause:  true,
					LastCause:   true,
				},
				{
					Number:      3,
					Content:     "",
					Highlighted: "",
				},
			},
		},
		Offset: 123,
	}
	wantFindingSymfonySecret := types.SecretFinding{
		RuleID:    "symfony-default-secret",
		Category:  secret.CategorySymfony,
		Title:     "Symfony Default Secret",
		Severity:  "HIGH",
		StartLine: 2,
		EndLine:   2,
		Match:     "  secret: ******************************",
		Code: types.Code{
			Lines: []types.Line{
				{
					Number:      1,
					Content:     "parameters:",
					Highlighted: "parameters:",
					IsCause:     false,
					FirstCause:  false,
					LastCause:   false,
				},
				{
					Number:      2,
					Content:     "  secret: ******************************",
					Highlighted: "  secret: ******************************",
					IsCause:     true,
					FirstCause:  true,
					LastCause:   true,
				},
			},
		},
		Offset: 22,
	}
	wantMultiLine := types.SecretFinding{
		RuleID:    "multi-line-secret",
		Category:  "general",
		Title:     "Generic Rule",
		Severity:  "HIGH",
		StartLine: 2,
		EndLine:   2,
		Match:     "***************",
		Code: types.Code{
			Lines: []types.Line{
				{
					Number:      1,
					Content:     "123",
					Highlighted: "123",
				},
				{
					Number:      2,
					Content:     "***************",
					Highlighted: "***************",
					IsCause:     true,
					FirstCause:  true,
					LastCause:   true,
				},
				{
					Number:      3,
					Content:     "123",
					Highlighted: "123",
				},
			},
		},
		Offset: 4,
	}
	wantFindingTokenInsideJs := types.SecretFinding{
		RuleID:    "stripe-publishable-token",
		Category:  "Stripe",
		Title:     "Stripe Publishable Key",
		Severity:  "LOW",
		StartLine: 1,
		EndLine:   1,
		Match:     "){case a.ez.PRODUCTION:return\"********************************\";case a.ez.TEST:cas",
		Code: types.Code{
			Lines: []types.Line{
				{
					Number:      1,
					Content:     "){case a.ez.PRODUCTION:return\"********************************\";case a.ez.TEST:cas",
					Highlighted: "){case a.ez.PRODUCTION:return\"********************************\";case a.ez.TEST:cas",
					IsCause:     true,
					FirstCause:  true,
					LastCause:   true,
				},
			},
		},
		Offset: 4016,
	}
	wantFindingJWT := types.SecretFinding{
		RuleID:    "jwt-token",
		Category:  "JWT",
		Title:     "JWT token",
		Severity:  "MEDIUM",
		StartLine: 3,
		EndLine:   3,
		Match:     "jwt: ***********************************************************************************************************************************************************",
		Code: types.Code{
			Lines: []types.Line{
				{
					Number:      1,
					Content:     "asd",
					Highlighted: "asd",
				},
				{
					Number:      2,
					Content:     "aaaa",
					Highlighted: "aaaa",
				},
				{
					Number:      3,
					Content:     "jwt: ***********************************************************************************************************************************************************",
					Highlighted: "jwt: ***********************************************************************************************************************************************************",
					IsCause:     true,
					FirstCause:  true,
					LastCause:   true,
				},
				{
					Number:      4,
					Content:     "asda",
					Highlighted: "asda",
				},
			},
		},
		Offset: 14,
	}

	wantFindingAzureStorageAccountKey := types.SecretFinding{
		RuleID:    "azure-storage-account-key",
		Category:  secret.CategoryAzure,
		Title:     "Azure Storage Account Key",
		Severity:  "CRITICAL",
		StartLine: 1,
		EndLine:   1,
		Match:     "AccountKey=" + strings.Repeat("*", 88),
		Code: types.Code{
			Lines: []types.Line{
				{
					Number:      1,
					Content:     "AccountKey=" + strings.Repeat("*", 88),
					Highlighted: "AccountKey=" + strings.Repeat("*", 88),
					IsCause:     true,
					FirstCause:  true,
					LastCause:   true,
				},
			},
		},
		Offset: 11,
	}
	wantFindingAzureSASToken := types.SecretFinding{
		RuleID:    "azure-sas-token",
		Category:  secret.CategoryAzure,
		Title:     "Azure Shared Access Signature Token",
		Severity:  "HIGH",
		StartLine: 1,
		EndLine:   1,
		Match:     "1T00%3A00%3A00Z&spr=https&sig=" + strings.Repeat("*", 44),
		Code: types.Code{
			Lines: []types.Line{
				{
					Number:      1,
					Content:     "1T00%3A00%3A00Z&spr=https&sig=" + strings.Repeat("*", 44),
					Highlighted: "1T00%3A00%3A00Z&spr=https&sig=" + strings.Repeat("*", 44),
					IsCause:     true,
					FirstCause:  true,
					LastCause:   true,
				},
			},
		},
		Offset: 66,
	}
	wantFindingAzureDevOpsPAT := types.SecretFinding{
		RuleID:    "azure-devops-pat",
		Category:  secret.CategoryAzure,
		Title:     "Azure DevOps Personal Access Token",
		Severity:  "HIGH",
		StartLine: 1,
		EndLine:   1,
		Match:     "AZURE_DEVOPS_TOKEN=" + strings.Repeat("*", 52),
		Code: types.Code{
			Lines: []types.Line{
				{
					Number:      1,
					Content:     "AZURE_DEVOPS_TOKEN=" + strings.Repeat("*", 52),
					Highlighted: "AZURE_DEVOPS_TOKEN=" + strings.Repeat("*", 52),
					IsCause:     true,
					FirstCause:  true,
					LastCause:   true,
				},
			},
		},
		Offset: 19,
	}

	azureStructuredToken := strings.Repeat("*", 84)

	wantFindingAzureEntraClientSecret := types.SecretFinding{
		RuleID:    "azure-entra-client-secret",
		Category:  secret.CategoryAzure,
		Title:     "Azure Entra ID Client Secret",
		Severity:  "CRITICAL",
		StartLine: 1,
		EndLine:   1,
		Match:     "client_secret=" + strings.Repeat("*", 40),
		Code: types.Code{
			Lines: []types.Line{
				{
					Number:      1,
					Content:     "client_secret=" + strings.Repeat("*", 40),
					Highlighted: "client_secret=" + strings.Repeat("*", 40),
					IsCause:     true,
					FirstCause:  true,
					LastCause:   true,
				},
			},
		},
		Offset: 14,
	}
	wantFindingAzureContainerRegistryCredential := types.SecretFinding{
		RuleID:    "azure-container-registry-credential",
		Category:  secret.CategoryAzure,
		Title:     "Azure Container Registry Credential",
		Severity:  "CRITICAL",
		StartLine: 1,
		EndLine:   1,
		Match:     azureStructuredToken,
		Code: types.Code{
			Lines: []types.Line{
				{
					Number:      1,
					Content:     azureStructuredToken,
					Highlighted: azureStructuredToken,
					IsCause:     true,
					FirstCause:  true,
					LastCause:   true,
				},
			},
		},
		Offset: 0,
	}
	wantFindingAzureContainerRegistryPassword := types.SecretFinding{
		RuleID:    "azure-container-registry-password",
		Category:  secret.CategoryAzure,
		Title:     "Azure Container Registry Password",
		Severity:  "CRITICAL",
		StartLine: 1,
		EndLine:   1,
		Match:     "myregistry.azurecr.io password: " + strings.Repeat("*", 32),
		Code: types.Code{
			Lines: []types.Line{
				{
					Number:      1,
					Content:     "myregistry.azurecr.io password: " + strings.Repeat("*", 32),
					Highlighted: "myregistry.azurecr.io password: " + strings.Repeat("*", 32),
					IsCause:     true,
					FirstCause:  true,
					LastCause:   true,
				},
			},
		},
		Offset: 32,
	}
	wantFindingAzureAppConfigConnectionString := types.SecretFinding{
		RuleID:    "azure-app-config-connection-string",
		Category:  secret.CategoryAzure,
		Title:     "Azure App Configuration Connection String",
		Severity:  "CRITICAL",
		StartLine: 1,
		EndLine:   1,
		Match:     "Endpoint=https://mystore.azconfig.io;Id=iLiM;Secret=" + strings.Repeat("*", 36),
		Code: types.Code{
			Lines: []types.Line{
				{
					Number:      1,
					Content:     "Endpoint=https://mystore.azconfig.io;Id=iLiM;Secret=" + strings.Repeat("*", 36),
					Highlighted: "Endpoint=https://mystore.azconfig.io;Id=iLiM;Secret=" + strings.Repeat("*", 36),
					IsCause:     true,
					FirstCause:  true,
					LastCause:   true,
				},
			},
		},
		Offset: 52,
	}
	wantFindingAzureAIServicesKey := types.SecretFinding{
		RuleID:    "azure-ai-services-key",
		Category:  secret.CategoryAzure,
		Title:     "Azure AI Services Key",
		Severity:  "HIGH",
		StartLine: 1,
		EndLine:   1,
		Match:     azureStructuredToken,
		Code: types.Code{
			Lines: []types.Line{
				{
					Number:      1,
					Content:     azureStructuredToken,
					Highlighted: azureStructuredToken,
					IsCause:     true,
					FirstCause:  true,
					LastCause:   true,
				},
			},
		},
		Offset: 0,
	}
	wantFindingMavenSettingsPassword := types.SecretFinding{
		RuleID:    "maven-settings-password",
		Category:  secret.CategoryMaven,
		Title:     "Maven settings.xml password",
		Severity:  "HIGH",
		StartLine: 2,
		EndLine:   2,
		Match:     "<password>  ***********  </password>",
		Code: types.Code{
			Lines: []types.Line{
				{
					Number:      1,
					Content:     "<settings>",
					Highlighted: "<settings>",
				},
				{
					Number:      2,
					Content:     "<password>  ***********  </password>",
					Highlighted: "<password>  ***********  </password>",
					IsCause:     true,
					FirstCause:  true,
					LastCause:   true,
				},
				{
					Number:      3,
					Content:     "<passphrase>  *************  </passphrase>",
					Highlighted: "<passphrase>  *************  </passphrase>",
				},
			},
		},
		Offset: 23,
	}
	wantFindingMavenSettingsPassphrase := types.SecretFinding{
		RuleID:    "maven-settings-passphrase",
		Category:  secret.CategoryMaven,
		Title:     "Maven settings.xml passphrase",
		Severity:  "HIGH",
		StartLine: 3,
		EndLine:   3,
		Match:     "<passphrase>  *************  </passphrase>",
		Code: types.Code{
			Lines: []types.Line{
				{
					Number:      1,
					Content:     "<settings>",
					Highlighted: "<settings>",
				},
				{
					Number:      2,
					Content:     "<password>  ***********  </password>",
					Highlighted: "<password>  ***********  </password>",
				},
				{
					Number:      3,
					Content:     "<passphrase>  *************  </passphrase>",
					Highlighted: "<passphrase>  *************  </passphrase>",
					IsCause:     true,
					FirstCause:  true,
					LastCause:   true,
				},
				{
					Number:      4,
					Content:     "</settings>",
					Highlighted: "</settings>",
				},
			},
		},
		Offset: 62,
	}
	wantFindingMavenSettingsSecurityMaster := types.SecretFinding{
		RuleID:    "maven-settings-security-master",
		Category:  secret.CategoryMaven,
		Title:     "Maven settings-security.xml master password",
		Severity:  "HIGH",
		StartLine: 2,
		EndLine:   2,
		Match:     "<master>***********</master>",
		Code: types.Code{
			Lines: []types.Line{
				{
					Number:      1,
					Content:     "<settingsSecurity>",
					Highlighted: "<settingsSecurity>",
				},
				{
					Number:      2,
					Content:     "<master>***********</master>",
					Highlighted: "<master>***********</master>",
					IsCause:     true,
					FirstCause:  true,
					LastCause:   true,
				},
				{
					Number:      3,
					Content:     "</settingsSecurity>",
					Highlighted: "</settingsSecurity>",
				},
			},
		},
		Offset: 27,
	}

	tests := []struct {
		name          string
		configPath    string
		inputFilePath string
		want          types.Secret
	}{
		{
			name:          "find match",
			configPath:    filepath.Join("testdata", "config.yaml"),
			inputFilePath: filepath.Join("testdata", "secret.txt"),
			want: types.Secret{
				FilePath: filepath.Join("testdata", "secret.txt"),
				Findings: []types.SecretFinding{
					wantFinding1,
					wantFinding2,
				},
			},
		},
		{
			name:          "find aws secrets",
			configPath:    filepath.Join("testdata", "config.yaml"),
			inputFilePath: filepath.Join("testdata", "aws-secrets.txt"),
			want: types.Secret{
				FilePath: filepath.Join("testdata", "aws-secrets.txt"),
				Findings: []types.SecretFinding{
					wantFinding5,
					wantFinding10,
					wantFinding9,
				},
			},
		},
		{
			name:          "find secrets in CRLF line endings file",
			configPath:    filepath.Join("testdata", "skip-test.yaml"),
			inputFilePath: filepath.Join("testdata", "crlf-line-endings.txt"),
			want: types.Secret{
				FilePath: filepath.Join("testdata", "crlf-line-endings.txt"),
				Findings: []types.SecretFinding{
					{
						RuleID:    "aws-access-key-id",
						Category:  secret.CategoryAWS,
						Title:     "AWS Access Key ID",
						Severity:  "CRITICAL",
						StartLine: 2,
						EndLine:   2,
						Code: types.Code{
							Lines: []types.Line{
								{
									Number:      1,
									Content:     "This is a test file with CRLF line endings.",
									IsCause:     false,
									Highlighted: "This is a test file with CRLF line endings.",
									FirstCause:  false,
									LastCause:   false,
								},
								{
									Number:      2,
									Content:     "AWS Access Key ID: ********************",
									IsCause:     true,
									Highlighted: "AWS Access Key ID: ********************",
									FirstCause:  true,
									LastCause:   true,
								},
								{
									Number:      3,
									Content:     "This line has no secrets.",
									IsCause:     false,
									Highlighted: "This line has no secrets.",
									FirstCause:  false,
									LastCause:   false,
								},
							},
						},
						Match:  "AWS Access Key ID: ********************",
						Offset: 63,
					},
					{
						RuleID:    "github-pat",
						Category:  secret.CategoryGitHub,
						Title:     "GitHub Personal Access Token",
						Severity:  "CRITICAL",
						StartLine: 4,
						EndLine:   4,
						Code: types.Code{
							Lines: []types.Line{
								{
									Number:      2,
									Content:     "AWS Access Key ID: ********************",
									IsCause:     false,
									Highlighted: "AWS Access Key ID: ********************",
									FirstCause:  false,
									LastCause:   false,
								},
								{
									Number:      3,
									Content:     "This line has no secrets.",
									IsCause:     false,
									Highlighted: "This line has no secrets.",
									FirstCause:  false,
									LastCause:   false,
								},
								{
									Number:      4,
									Content:     "GitHub PAT: ****************************************12",
									IsCause:     true,
									Highlighted: "GitHub PAT: ****************************************12",
									FirstCause:  true,
									LastCause:   true,
								},
								{
									Number:      5,
									Content:     "End of file.",
									IsCause:     false,
									Highlighted: "End of file.",
									FirstCause:  false,
									LastCause:   false,
								},
							},
						},
						Match:  "GitHub PAT: ****************************************12",
						Offset: 122,
					},
				},
			},
		},
		{
			name:          "find Asymmetric Private Key secrets",
			configPath:    filepath.Join("testdata", "skip-test.yaml"),
			inputFilePath: filepath.Join("testdata", "asymmetric-private-secret.txt"),
			want: types.Secret{
				FilePath: filepath.Join("testdata", "asymmetric-private-secret.txt"),
				Findings: []types.SecretFinding{wantFindingAsymmetricPrivateKey},
			},
		},
		{
			name:          "find Alibaba AccessKey ID txt",
			configPath:    filepath.Join("testdata", "skip-test.yaml"),
			inputFilePath: "testdata/alibaba-access-key-id.txt",
			want: types.Secret{
				FilePath: "testdata/alibaba-access-key-id.txt",
				Findings: []types.SecretFinding{wantFindingAlibabaAccessKeyId},
			},
		},
		{
			name:          "find Asymmetric Private Key secrets json",
			configPath:    filepath.Join("testdata", "skip-test.yaml"),
			inputFilePath: filepath.Join("testdata", "asymmetric-private-secret.json"),
			want: types.Secret{
				FilePath: filepath.Join("testdata", "asymmetric-private-secret.json"),
				Findings: []types.SecretFinding{wantFindingAsymmetricPrivateKeyJson},
			},
		},
		{
			name:          "find Docker registry credentials",
			configPath:    filepath.Join("testdata", "skip-test.yaml"),
			inputFilePath: filepath.Join("testdata", "docker-secrets.txt"),
			want: types.Secret{
				FilePath: filepath.Join("testdata", "docker-secrets.txt"),
				Findings: []types.SecretFinding{
					wantFindingDockerKey1,
					wantFindingDockerKey2,
				},
			},
		},
		{
			name:          "find Hugging face secret",
			configPath:    filepath.Join("testdata", "config.yaml"),
			inputFilePath: filepath.Join("testdata", "hugging-face-secret.txt"),
			want: types.Secret{
				FilePath: filepath.Join("testdata", "hugging-face-secret.txt"),
				Findings: []types.SecretFinding{wantFindingHuggingFace},
			},
		},
		{
			name:          "find grafana secret",
			configPath:    filepath.Join("testdata", "config.yaml"),
			inputFilePath: filepath.Join("testdata", "grafana-env.txt"),
			want: types.Secret{
				FilePath: filepath.Join("testdata", "grafana-env.txt"),
				Findings: []types.SecretFinding{
					wantFindingGrafanaUnquoted,
					wantFindingGrafanaQuoted,
				},
			},
		},
		{
			name:          "find JWT token",
			configPath:    filepath.Join("testdata", "config.yaml"),
			inputFilePath: filepath.Join("testdata", "jwt-secret.txt"),
			want: types.Secret{
				FilePath: filepath.Join("testdata", "jwt-secret.txt"),
				Findings: []types.SecretFinding{wantFindingJWT},
			},
		},
		{
			name:          "find Symfony default secret",
			configPath:    filepath.Join("testdata", "config.yaml"),
			inputFilePath: filepath.Join("testdata", "symfony-secret.yaml"),
			want: types.Secret{
				FilePath: filepath.Join("testdata", "symfony-secret.yaml"),
				Findings: []types.SecretFinding{wantFindingSymfonySecret},
			},
		},
		{
			name:          "find Private Packagist tokens",
			configPath:    filepath.Join("testdata", "config.yaml"),
			inputFilePath: filepath.Join("testdata", "private-packagist.txt"),
			want: types.Secret{
				FilePath: filepath.Join("testdata", "private-packagist.txt"),
				Findings: []types.SecretFinding{
					wantFindingPrivatePackagistOrgReadToken,
					wantFindingPrivatePackagistOrgUpdateToken,
					wantFindingPrivatePackagistUserToken,
				},
			},
		},
		{
			name:          "include when keyword found",
			configPath:    filepath.Join("testdata", "config-happy-keywords.yaml"),
			inputFilePath: filepath.Join("testdata", "secret.txt"),
			want: types.Secret{
				FilePath: filepath.Join("testdata", "secret.txt"),
				Findings: []types.SecretFinding{
					wantFinding1,
					wantFinding2,
				},
			},
		},
		{
			name:          "exclude when no keyword found",
			configPath:    filepath.Join("testdata", "config-sad-keywords.yaml"),
			inputFilePath: filepath.Join("testdata", "secret.txt"),
			want:          types.Secret{},
		},
		{
			name:          "should ignore .md files by default",
			configPath:    filepath.Join("testdata", "config.yaml"),
			inputFilePath: filepath.Join("testdata", "secret.md"),
			want: types.Secret{
				FilePath: filepath.Join("testdata", "secret.md"),
			},
		},
		{
			name:          "should disable .md allow rule",
			configPath:    filepath.Join("testdata", "config-disable-allow-rule-md.yaml"),
			inputFilePath: filepath.Join("testdata", "secret.md"),
			want: types.Secret{
				FilePath: filepath.Join("testdata", "secret.md"),
				Findings: []types.SecretFinding{
					wantFinding1,
					wantFinding2,
				},
			},
		},
		{
			name:          "should find ghp builtin secret",
			configPath:    filepath.Join("testdata", "skip-test.yaml"),
			inputFilePath: filepath.Join("testdata", "builtin-rule-secret.txt"),
			want: types.Secret{
				FilePath: filepath.Join("testdata", "builtin-rule-secret.txt"),
				Findings: []types.SecretFinding{
					wantFinding5a,
					wantFinding6,
				},
			},
		},
		{
			name:          "should find GitHub Personal Access Token (classic)",
			configPath:    filepath.Join("testdata", "skip-test.yaml"),
			inputFilePath: "testdata/github-token.txt",
			want: types.Secret{
				FilePath: "testdata/github-token.txt",
				Findings: []types.SecretFinding{wantFindingGitHubPAT},
			},
		},
		{
			name:          "should enable github-pat builtin rule, but disable aws-access-key-id rule",
			configPath:    filepath.Join("testdata", "config-enable-ghp.yaml"),
			inputFilePath: filepath.Join("testdata", "builtin-rule-secret.txt"),
			want: types.Secret{
				FilePath: filepath.Join("testdata", "builtin-rule-secret.txt"),
				Findings: []types.SecretFinding{wantFindingGHButDisableAWS},
			},
		},
		{
			name:          "should disable github-pat builtin rule",
			configPath:    filepath.Join("testdata", "config-disable-ghp.yaml"),
			inputFilePath: filepath.Join("testdata", "builtin-rule-secret.txt"),
			want: types.Secret{
				FilePath: filepath.Join("testdata", "builtin-rule-secret.txt"),
				Findings: []types.SecretFinding{wantFindingPATDisabled},
			},
		},
		{
			name:          "should disable custom rule",
			configPath:    filepath.Join("testdata", "config-disable-rule1.yaml"),
			inputFilePath: filepath.Join("testdata", "secret.txt"),
			want:          types.Secret{},
		},
		{
			name:          "allow-rule path",
			configPath:    filepath.Join("testdata", "allow-path.yaml"),
			inputFilePath: filepath.Join("testdata", "secret.txt"),
			want:          types.Secret{},
		},
		{
			name:          "allow-rule regex inside group",
			configPath:    filepath.Join("testdata", "allow-regex.yaml"),
			inputFilePath: filepath.Join("testdata", "secret.txt"),
			want: types.Secret{
				FilePath: filepath.Join("testdata", "secret.txt"),
				Findings: []types.SecretFinding{wantFinding1},
			},
		},
		{
			name:          "allow-rule regex outside group",
			configPath:    filepath.Join("testdata", "allow-regex-outside-group.yaml"),
			inputFilePath: filepath.Join("testdata", "secret.txt"),
			want:          types.Secret{},
		},
		{
			name:          "exclude-block regexes",
			configPath:    filepath.Join("testdata", "exclude-block.yaml"),
			inputFilePath: filepath.Join("testdata", "secret.txt"),
			want: types.Secret{
				FilePath: filepath.Join("testdata", "secret.txt"),
				Findings: []types.SecretFinding{wantFindingRegexDisabled},
			},
		},
		{
			name:          "skip examples file",
			configPath:    filepath.Join("testdata", "skip-test.yaml"),
			inputFilePath: filepath.Join("testdata", "example-secret.txt"),
			want: types.Secret{
				FilePath: filepath.Join("testdata", "example-secret.txt"),
			},
		},
		{
			name:          "global allow-rule path",
			configPath:    filepath.Join("testdata", "global-allow-path.yaml"),
			inputFilePath: filepath.Join("testdata", "secret.txt"),
			want: types.Secret{
				FilePath: filepath.Join("testdata", "secret.txt"),
				Findings: nil,
			},
		},
		{
			name:          "global allow-rule regex",
			configPath:    filepath.Join("testdata", "global-allow-regex.yaml"),
			inputFilePath: filepath.Join("testdata", "secret.txt"),
			want: types.Secret{
				FilePath: filepath.Join("testdata", "secret.txt"),
				Findings: []types.SecretFinding{wantFinding1},
			},
		},
		{
			name:          "global exclude-block regexes",
			configPath:    filepath.Join("testdata", "global-exclude-block.yaml"),
			inputFilePath: filepath.Join("testdata", "secret.txt"),
			want: types.Secret{
				FilePath: filepath.Join("testdata", "secret.txt"),
				Findings: []types.SecretFinding{wantFindingRegexDisabled},
			},
		},
		{
			name:          "multiple secret groups",
			configPath:    filepath.Join("testdata", "multiple-secret-groups.yaml"),
			inputFilePath: filepath.Join("testdata", "secret.txt"),
			want: types.Secret{
				FilePath: filepath.Join("testdata", "secret.txt"),
				Findings: []types.SecretFinding{
					wantFindingMultipleGroupsUsername,
					wantFindingMultipleGroupsPassword,
				},
			},
		},
		{
			name:          "truncate long line",
			configPath:    filepath.Join("testdata", "skip-test.yaml"),
			inputFilePath: filepath.Join("testdata", "long-line-secret.txt"),
			want: types.Secret{
				FilePath: filepath.Join("testdata", "long-line-secret.txt"),
				Findings: []types.SecretFinding{wantFinding7},
			},
		},
		{
			name:          "add unknown severity when rule has no severity",
			configPath:    filepath.Join("testdata", "config-without-severity.yaml"),
			inputFilePath: filepath.Join("testdata", "secret.txt"),
			want: types.Secret{
				FilePath: filepath.Join("testdata", "secret.txt"),
				Findings: []types.SecretFinding{wantFinding8},
			},
		},
		{
			name:          "add unknown severity when rule has no severity",
			configPath:    filepath.Join("testdata", "config-with-incorrect-severity.yaml"),
			inputFilePath: filepath.Join("testdata", "secret.txt"),
			want: types.Secret{
				FilePath: filepath.Join("testdata", "secret.txt"),
				Findings: []types.SecretFinding{wantFinding8},
			},
		},
		{
			name:          "update severity if rule severity is not in uppercase",
			configPath:    filepath.Join("testdata", "config-with-non-uppercase-severity.yaml"),
			inputFilePath: filepath.Join("testdata", "secret.txt"),
			want: types.Secret{
				FilePath: filepath.Join("testdata", "secret.txt"),
				Findings: []types.SecretFinding{wantFinding8},
			},
		},
		{
			name:          "use unknown severity when rule has incorrect severity",
			configPath:    filepath.Join("testdata", "config-with-incorrect-severity.yaml"),
			inputFilePath: filepath.Join("testdata", "secret.txt"),
			want: types.Secret{
				FilePath: filepath.Join("testdata", "secret.txt"),
				Findings: []types.SecretFinding{wantFinding8},
			},
		},
		{
			name:          "invalid aws secrets",
			configPath:    filepath.Join("testdata", "skip-test.yaml"),
			inputFilePath: filepath.Join("testdata", "invalid-aws-secrets.txt"),
			want:          types.Secret{},
		},
		{
			name:          "secret inside another word",
			configPath:    filepath.Join("testdata", "skip-test.yaml"),
			inputFilePath: filepath.Join("testdata", "wrapped-secrets.txt"),
			want:          types.Secret{},
		},
		{
			name:          "sensitive secret inside another word",
			configPath:    filepath.Join("testdata", "skip-test.yaml"),
			inputFilePath: filepath.Join("testdata", "wrapped-secrets-sensitive.txt"),
			want: types.Secret{
				FilePath: filepath.Join("testdata", "wrapped-secrets-sensitive.txt"),
				Findings: []types.SecretFinding{
					wantFindingMyAwsAccessKey,
					wantFindingMyGitHubPAT,
				},
			},
		},
		{
			name:          "asymmetric file",
			configPath:    filepath.Join("testdata", "skip-test.yaml"),
			inputFilePath: "testdata/asymmetric-private-key.txt",
			want: types.Secret{
				FilePath: "testdata/asymmetric-private-key.txt",
				Findings: []types.SecretFinding{
					wantFindingAsymmSecretKey,
					wantFindingMinimumAsymmSecretKey,
				},
			},
		},
		{
			name:          "begin/end line symbols without multi-line mode",
			configPath:    filepath.Join("testdata", "multi-line-off.yaml"),
			inputFilePath: "testdata/multi-line.txt",
			want:          types.Secret{},
		},
		{
			name:          "begin/end line symbols with multi-line mode",
			configPath:    filepath.Join("testdata", "multi-line-on.yaml"),
			inputFilePath: "testdata/multi-line.txt",
			want: types.Secret{
				FilePath: "testdata/multi-line.txt",
				Findings: []types.SecretFinding{wantMultiLine},
			},
		},
		{
			name:          "long obfuscated js code with secrets",
			configPath:    filepath.Join("testdata", "skip-test.yaml"),
			inputFilePath: filepath.Join("testdata", "obfuscated.js"),
			want: types.Secret{
				FilePath: filepath.Join("testdata", "obfuscated.js"),
				Findings: []types.SecretFinding{wantFindingTokenInsideJs},
			},
		},
		{
			name:          "find Azure Storage Account Key",
			configPath:    filepath.Join("testdata", "skip-test.yaml"),
			inputFilePath: filepath.Join("testdata", "azure-storage-account-key.txt"),
			want: types.Secret{
				FilePath: filepath.Join("testdata", "azure-storage-account-key.txt"),
				Findings: []types.SecretFinding{wantFindingAzureStorageAccountKey},
			},
		},
		{
			name:          "find Azure SAS Token",
			configPath:    filepath.Join("testdata", "skip-test.yaml"),
			inputFilePath: filepath.Join("testdata", "azure-sas-token.txt"),
			want: types.Secret{
				FilePath: filepath.Join("testdata", "azure-sas-token.txt"),
				Findings: []types.SecretFinding{wantFindingAzureSASToken},
			},
		},
		{
			name:          "find Azure DevOps PAT",
			configPath:    filepath.Join("testdata", "skip-test.yaml"),
			inputFilePath: filepath.Join("testdata", "azure-devops-pat.txt"),
			want: types.Secret{
				FilePath: filepath.Join("testdata", "azure-devops-pat.txt"),
				Findings: []types.SecretFinding{wantFindingAzureDevOpsPAT},
			},
		},
		{
			name:          "find Azure Entra ID Client Secret",
			configPath:    filepath.Join("testdata", "skip-test.yaml"),
			inputFilePath: filepath.Join("testdata", "azure-entra-client-secret.txt"),
			want: types.Secret{
				FilePath: filepath.Join("testdata", "azure-entra-client-secret.txt"),
				Findings: []types.SecretFinding{wantFindingAzureEntraClientSecret},
			},
		},
		{
			name:          "find Azure Container Registry Credential",
			configPath:    filepath.Join("testdata", "skip-test.yaml"),
			inputFilePath: filepath.Join("testdata", "azure-container-registry-credential.txt"),
			want: types.Secret{
				FilePath: filepath.Join("testdata", "azure-container-registry-credential.txt"),
				Findings: []types.SecretFinding{wantFindingAzureContainerRegistryCredential},
			},
		},
		{
			name:          "find Azure Container Registry Password",
			configPath:    filepath.Join("testdata", "skip-test.yaml"),
			inputFilePath: filepath.Join("testdata", "azure-container-registry-password.txt"),
			want: types.Secret{
				FilePath: filepath.Join("testdata", "azure-container-registry-password.txt"),
				Findings: []types.SecretFinding{wantFindingAzureContainerRegistryPassword},
			},
		},
		{
			name:          "find Azure App Configuration Connection String",
			configPath:    filepath.Join("testdata", "skip-test.yaml"),
			inputFilePath: filepath.Join("testdata", "azure-app-config-connection-string.txt"),
			want: types.Secret{
				FilePath: filepath.Join("testdata", "azure-app-config-connection-string.txt"),
				Findings: []types.SecretFinding{wantFindingAzureAppConfigConnectionString},
			},
		},
		{
			name:          "find Azure AI Services Key",
			configPath:    filepath.Join("testdata", "skip-test.yaml"),
			inputFilePath: filepath.Join("testdata", "azure-ai-services-key.txt"),
			want: types.Secret{
				FilePath: filepath.Join("testdata", "azure-ai-services-key.txt"),
				Findings: []types.SecretFinding{wantFindingAzureAIServicesKey},
			},
		},
		{
			name:          "find Maven settings.xml password and passphrase",
			configPath:    filepath.Join("testdata", "skip-test.yaml"),
			inputFilePath: filepath.Join("testdata", "settings.xml"),
			want: types.Secret{
				FilePath: filepath.Join("testdata", "settings.xml"),
				Findings: []types.SecretFinding{
					wantFindingMavenSettingsPassphrase,
					wantFindingMavenSettingsPassword,
				},
			},
		},
		{
			name:          "find Maven settings-security.xml master password",
			configPath:    filepath.Join("testdata", "skip-test.yaml"),
			inputFilePath: filepath.Join("testdata", "settings-security.xml"),
			want: types.Secret{
				FilePath: filepath.Join("testdata", "settings-security.xml"),
				Findings: []types.SecretFinding{wantFindingMavenSettingsSecurityMaster},
			},
		},
		{
			// Three classes of non-secret values must be skipped:
			//   1. Maven-encrypted (`{...}`) — safe without the master, which a separate
			//      rule catches.
			//   2. Maven property substitution (`${env.X}`) — a reference, not a literal.
			//   3. Empty or whitespace-only values — fail the minimum-length check.
			name:          "skip non-secret values (encrypted, placeholders, empty)",
			configPath:    filepath.Join("testdata", "skip-test.yaml"),
			inputFilePath: filepath.Join("testdata", "settings-encrypted.xml"),
			want:          types.Secret{},
		},
		{
			name:          "invalid UTF-8 sequences in secrets",
			configPath:    filepath.Join("testdata", "skip-test.yaml"),
			inputFilePath: filepath.Join("testdata", "invalid-utf8.txt"),
			want: types.Secret{
				FilePath: filepath.Join("testdata", "invalid-utf8.txt"),
				Findings: []types.SecretFinding{
					{
						RuleID:    "github-pat",
						Category:  secret.CategoryGitHub,
						Title:     "GitHub Personal Access Token",
						Severity:  "CRITICAL",
						StartLine: 1,
						EndLine:   1,
						Match:     "token=****************************************",
						Code: types.Code{
							Lines: []types.Line{
								{
									Number:      1,
									Content:     "token=****************************************",
									Highlighted: "token=****************************************",
									IsCause:     true,
									FirstCause:  true,
									LastCause:   true,
								},
								{
									Number:      2,
									Content:     "# Comment with invalid UTF-8: �",
									Highlighted: "# Comment with invalid UTF-8: �",
								},
							},
						},
						Offset: 6,
					},
				},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			content, err := os.ReadFile(tt.inputFilePath)
			require.NoError(t, err)

			content = bytes.ReplaceAll(content, []byte("\r"), []byte(""))

			c, err := secret.ParseConfig(tt.configPath)
			require.NoError(t, err)

			s := secret.NewScanner(c)
			got := s.Scan(secret.ScanArgs{
				FilePath: tt.inputFilePath,
				Content:  bytes.NewReader(content),
			})
			assert.Equal(t, tt.want, got)
		})
	}
}

func TestSecretScannerWithStreaming(t *testing.T) {
	tests := []struct {
		name        string
		input       string
		bufferSize  int
		overlapSize int
		configPath  string
		want        types.Secret
	}{
		{
			name: "secret in second chunk",
			input: strings.Repeat("x", 520) + "\n" + // 520 bytes to push secret to second chunk
				"AWS_ACCESS_KEY_ID=AKIA0123456789ABCDEF\n" + // at offset 521
				strings.Repeat("y", 100), // padding
			bufferSize:  512,
			overlapSize: 128,
			configPath:  filepath.Join("testdata", "skip-test.yaml"),
			want: types.Secret{
				FilePath: "test.txt",
				Findings: []types.SecretFinding{
					{
						RuleID:    "aws-access-key-id",
						Category:  secret.CategoryAWS,
						Title:     "AWS Access Key ID",
						Severity:  "CRITICAL",
						StartLine: 2,
						EndLine:   2,
						Match:     "AWS_ACCESS_KEY_ID=********************",
						Offset:    539, // 521 + 18 (position of the actual key value)
					},
				},
			},
		},
		{
			name: "secret spanning chunk boundary",
			input: strings.Repeat("x", 480) + "\n" + // 480 bytes
				"AWS_ACCESS_KEY_ID=AKIA0123456789ABCDEF\n" + // at offset 481, spans chunk boundary at 512
				strings.Repeat("y", 200), // padding
			bufferSize:  512, // Boundary will be in the middle of the secret
			overlapSize: 128,
			configPath:  filepath.Join("testdata", "skip-test.yaml"),
			want: types.Secret{
				FilePath: "test.txt",
				Findings: []types.SecretFinding{
					{
						RuleID:    "aws-access-key-id",
						Category:  secret.CategoryAWS,
						Title:     "AWS Access Key ID",
						Severity:  "CRITICAL",
						StartLine: 2,
						EndLine:   2,
						Match:     "AWS_ACCESS_KEY_ID=********************",
						Offset:    499, // 481 + 18
					},
				},
			},
		},
		{
			name: "multiple secrets across chunks",
			input: "GITHUB_PAT=ghp_012345678901234567890123456789abcdef\n" + // at offset 0, 52 bytes
				strings.Repeat("x", 1200) + "\n" + // 1200 bytes padding
				"AWS_ACCESS_KEY_ID=AKIA0123456789ABCDEF\n" + // at offset 1253
				strings.Repeat("y", 1400) + "\n" + // 1400 bytes padding
				"stripe_key=sk_test_51H5Z3jGXvP5CVwYOLLllllllllllllllllllllllll\n" + // at offset 2693
				strings.Repeat("z", 200), // final padding
			bufferSize:  1024,
			overlapSize: 256,
			configPath:  filepath.Join("testdata", "skip-test.yaml"),
			want: types.Secret{
				FilePath: "test.txt",
				Findings: []types.SecretFinding{
					{
						RuleID:    "aws-access-key-id",
						Category:  secret.CategoryAWS,
						Title:     "AWS Access Key ID",
						Severity:  "CRITICAL",
						StartLine: 3,
						EndLine:   3,
						Match:     "AWS_ACCESS_KEY_ID=********************",
						Offset:    1271, // 1253 + 18
					},
					{
						RuleID:    "github-pat",
						Category:  secret.CategoryGitHub,
						Title:     "GitHub Personal Access Token",
						Severity:  "CRITICAL",
						StartLine: 1,
						EndLine:   1,
						Match:     "GITHUB_PAT=****************************************", // 40 asterisks for 40-char token
						Offset:    11,                                                    // position of the token value
					},
					{
						RuleID:    "stripe-secret-token",
						Category:  secret.CategoryStripe,
						Title:     "Stripe Secret Key",
						Severity:  "CRITICAL",
						StartLine: 5,
						EndLine:   5,
						Match:     "stripe_key=****************************************lllllllllll", // Stripe key pattern includes literal 'l' chars
						Offset:    2704,                                                             // 2693 + 11
					},
				},
			},
		},
		{
			name: "secret at exact chunk boundary",
			input: strings.Repeat("x", 383) + "\n" + // 383 bytes + newline = 384
				"AWS_ACCESS_KEY_ID=AKIA0123456789ABCDEF", // Starts exactly at chunk boundary (384)
			bufferSize:  384, // Boundary right after the newline
			overlapSize: 96,
			configPath:  filepath.Join("testdata", "skip-test.yaml"),
			want: types.Secret{
				FilePath: "test.txt",
				Findings: []types.SecretFinding{
					{
						RuleID:    "aws-access-key-id",
						Category:  secret.CategoryAWS,
						Title:     "AWS Access Key ID",
						Severity:  "CRITICAL",
						StartLine: 2,
						EndLine:   2,
						Match:     "AWS_ACCESS_KEY_ID=********************",
						Offset:    402, // 384 + 18
					},
				},
			},
		},
		{
			name: "very small buffer and overlap",
			input: strings.Repeat("x", 150) + "\n" + // 150 bytes padding
				"secret=\"mysecret123\"\n" + // at offset 151
				strings.Repeat("y", 345) + "\n" + // 345 bytes padding
				"secret=\"anothersecret456\"\n" + // at offset 517
				strings.Repeat("z", 150), // suffix padding
			bufferSize:  128,
			overlapSize: 32,
			configPath:  filepath.Join("testdata", "config.yaml"),
			want: types.Secret{
				FilePath: "test.txt",
				Findings: []types.SecretFinding{
					{
						RuleID:    "rule1",
						Category:  "general",
						Title:     "Generic Rule",
						Severity:  "HIGH",
						StartLine: 2,
						EndLine:   2,
						Match:     "secret=\"***********\"",
						Offset:    159, // 151 + 8 (position of "mysecret123")
					},
					{
						RuleID:    "rule1",
						Category:  "general",
						Title:     "Generic Rule",
						Severity:  "HIGH",
						StartLine: 4,
						EndLine:   4,
						Match:     "secret=\"****************\"",
						Offset:    526, // 517 + 9
					},
				},
			},
		},
		{
			name: "multi-line secret with small chunks",
			input: strings.Repeat("x", 720) + "\n" + // 720 bytes padding
				"-----BEGIN RSA PRIVATE KEY-----\n" + // at offset 721
				"MIIEpAIBAAKCAQEA1234567890abcdefghijklmnopqrstuvwxyz\n" +
				"ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnop\n" +
				"qrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ12345678\n" +
				"-----END RSA PRIVATE KEY-----\n" +
				strings.Repeat("z", 460), // suffix padding
			bufferSize:  1024, // Ensure the entire key fits in one chunk
			overlapSize: 256,
			configPath:  filepath.Join("testdata", "skip-test.yaml"),
			want: types.Secret{
				FilePath: "test.txt",
				Findings: []types.SecretFinding{
					{
						RuleID:    "private-key",
						Category:  secret.CategoryAsymmetricPrivateKey,
						Title:     "Asymmetric Private Key",
						Severity:  "HIGH",
						StartLine: 3,
						EndLine:   5,
						Match:     "****************************************************", // Multi-line secret content
						Offset:    753,                                                    // 721 + 32 (after BEGIN line)
					},
				},
			},
		},
		{
			name: "multi-line secret exceeding overlap size (known limitation)",
			input: strings.Repeat("x", 920) + "\n" + // Position key at chunk boundary (1024 - ~100 bytes for key)
				"-----BEGIN RSA PRIVATE KEY-----\n" + // at offset 921
				"MIIEpAIBAAKCAQEA1234567890abcdefghijklmnopqrstuvwxyz\n" +
				"ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnop\n" +
				"qrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ12345678\n" +
				"-----END RSA PRIVATE KEY-----\n" +
				strings.Repeat("z", 460), // suffix padding
			bufferSize:  1024,
			overlapSize: 100, // Too small to capture the entire key
			configPath:  filepath.Join("testdata", "skip-test.yaml"),
			want: types.Secret{
				FilePath: "test.txt",
				Findings: nil, // Key won't be detected as it spans beyond the overlap
			},
		},
		{
			name:        "no secrets in any chunk",
			input:       strings.Repeat("this is just normal content without any secrets in it at all\n", 50),
			bufferSize:  512,
			overlapSize: 128, // 1/4 of buffer
			configPath:  filepath.Join("testdata", "config.yaml"),
			want: types.Secret{
				FilePath: "test.txt",
				Findings: nil,
			},
		},
		{
			name: "secret in last chunk with EOF",
			input: strings.Repeat("x", 1500) + "\n" + // 1500 bytes to push secret to end
				"final_secret=\"supersecretvalue\"", // at offset 1501, no newline at end
			bufferSize:  640,
			overlapSize: 160, // 1/4 of buffer
			configPath:  filepath.Join("testdata", "config.yaml"),
			want: types.Secret{
				FilePath: "test.txt",
				Findings: []types.SecretFinding{
					{
						RuleID:    "rule1",
						Category:  "general",
						Title:     "Generic Rule",
						Severity:  "HIGH",
						StartLine: 2,
						EndLine:   2,
						Match:     "final_secret=\"****************\"",
						Offset:    1515, // 1501 + 14 (position of "supersecretvalue")
					},
				},
			},
		},
		{
			name: "deduplicate findings at chunk boundaries",
			input: strings.Repeat("x", 330) + "\n" + // 330 bytes
				"secret=\"duplicatetest123\"\n" + // at offset 331, ends at 356
				strings.Repeat("y", 200), // padding
			bufferSize:  400, // First chunk: 0-399, overlap: 300-399
			overlapSize: 100, // Secret (331-356) is within overlap region
			configPath:  filepath.Join("testdata", "config.yaml"),
			want: types.Secret{
				FilePath: "test.txt",
				Findings: []types.SecretFinding{
					{
						RuleID:    "rule1",
						Category:  "general",
						Title:     "Generic Rule",
						Severity:  "HIGH",
						StartLine: 2,
						EndLine:   2,
						Match:     "secret=\"****************\"",
						Offset:    339, // 331 + 8 (position of "duplicatetest123")
					},
				},
			},
		},
		{
			name: "multiple secrets on same line",
			input: strings.Repeat("x", 480) + "\n" + // 480 bytes padding
				"secret=\"first123\" and secret=\"second456\" on same line\n" + // at offset 481
				strings.Repeat("y", 1300), // large padding to force multiple chunks
			bufferSize:  512,
			overlapSize: 128, // 1/4 of buffer
			configPath:  filepath.Join("testdata", "config.yaml"),
			want: types.Secret{
				FilePath: "test.txt",
				Findings: []types.SecretFinding{
					{
						RuleID:    "rule1",
						Category:  "general",
						Title:     "Generic Rule",
						Severity:  "HIGH",
						StartLine: 2,
						EndLine:   2,
						Match:     "secret=\"********\" and secret=\"*********\" on same line", // Full line match for second secret
						Offset:    511,                                                         // 481 + 30 (position of "second456")
					},
					{
						RuleID:    "rule1",
						Category:  "general",
						Title:     "Generic Rule",
						Severity:  "HIGH",
						StartLine: 2,
						EndLine:   2,
						Match:     "secret=\"********\" and secret=\"s", // Truncated match for first secret
						Offset:    489,                                  // 481 + 8 (position of "first123")
					},
				},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Parse config
			c, err := secret.ParseConfig(tt.configPath)
			require.NoError(t, err)

			// Create scanner with custom buffer and overlap sizes
			s := secret.NewScanner(c,
				secret.WithBufferSize(tt.bufferSize),
				secret.WithOverlapSize(tt.overlapSize))

			// Scan with streaming
			reader := strings.NewReader(tt.input)
			got := s.Scan(secret.ScanArgs{
				FilePath: "test.txt",
				Content:  reader,
			})

			// Clear Code field as it's too verbose to specify in test expectations
			for i := range got.Findings {
				got.Findings[i].Code = types.Code{}
			}
			for i := range tt.want.Findings {
				tt.want.Findings[i].Code = types.Code{}
			}

			// Compare all findings at once
			assert.Equal(t, tt.want.Findings, got.Findings, "unexpected findings")
		})
	}
}

func writeSecretConfig(t *testing.T, body string) string {
	t.Helper()
	path := filepath.Join(t.TempDir(), "trivy-secret.yaml")
	require.NoError(t, os.WriteFile(path, []byte(body), 0o644))
	return path
}

func TestParseConfig(t *testing.T) {
	tests := []struct {
		name    string
		body    string
		want    *[]string // nil == expect c.SkipPatterns == nil
		wantErr string
	}{
		{
			name: "missing skip-patterns leaves SkipPatterns nil",
			body: "disable-allow-rules:\n  - tests\n",
			want: nil,
		},
		{
			name: "empty skip-patterns yields non-nil empty slice",
			body: "skip-patterns: []\n",
			want: &[]string{},
		},
		{
			name: "valid forward-slash patterns are preserved",
			body: "skip-patterns:\n  - '**/.git/**'\n  - '**/node_modules/**'\n  - '**/go.sum'\n",
			want: &[]string{"**/.git/**", "**/node_modules/**", "**/go.sum"},
		},
		{
			name: "OS-native patterns are converted to forward slashes",
			// filepath.FromSlash uses the OS separator — backslash on Windows, no-op elsewhere.
			body: "skip-patterns:\n" +
				"  - '" + filepath.FromSlash("**/.git/**") + "'\n" +
				"  - '" + filepath.FromSlash("**/node_modules/**") + "'\n" +
				"  - '" + filepath.FromSlash("**/go.sum") + "'\n",
			want: &[]string{"**/.git/**", "**/node_modules/**", "**/go.sum"},
		},
		{
			name:    "invalid pattern returns error",
			body:    "skip-patterns:\n  - '[invalid'\n",
			wantErr: "invalid skip-pattern",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			path := writeSecretConfig(t, tt.body)
			c, err := secret.ParseConfig(path)
			if tt.wantErr != "" {
				require.Error(t, err)
				assert.Contains(t, err.Error(), tt.wantErr)
				return
			}
			require.NoError(t, err)
			require.NotNil(t, c)
			if tt.want == nil {
				assert.Nil(t, c.SkipPatterns)
				return
			}
			require.NotNil(t, c.SkipPatterns)
			assert.Equal(t, *tt.want, *c.SkipPatterns)
		})
	}
}

func TestIsSkippedSlashConversion(t *testing.T) {
	tests := []struct {
		name     string
		patterns []string
		path     string
		want     bool
	}{
		{
			name:     "forward slash path matches forward slash pattern",
			patterns: []string{"**/.git/**"},
			path:     "some/repo/.git/config",
			want:     true,
		},
		{
			name:     "OS-native path separator matches forward slash pattern",
			patterns: []string{"**/.git/**"},
			// filepath.FromSlash converts to OS separator — exercises ToSlash inside IsSkipped on Windows.
			path: filepath.FromSlash("some/repo/.git/config"),
			want: true,
		},
		{
			name:     "non-matching path returns false",
			patterns: []string{"**/.git/**"},
			path:     "some/repo/src/main.go",
			want:     false,
		},
		{
			name:     "OS-native path matches node_modules pattern",
			patterns: []string{"**/node_modules/**"},
			path:     filepath.FromSlash("project/node_modules/lodash/index.js"),
			want:     true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			g := secret.Global{SkipPatterns: tt.patterns}
			assert.Equal(t, tt.want, g.IsSkipped(tt.path))
		})
	}
}
