package sv

import (
	"reflect"
	"testing"
)

var ccfg = CommitMessageConfig{
	Types: []string{"feat", "fix"},
	Scope: CommitMessageScopeConfig{},
	Footer: map[string]CommitMessageFooterConfig{
		"issue": {Key: "jira", KeySynonyms: []string{"Jira"}},
		"refs":  {Key: "Refs", UseHash: true},
	},
	Issue: CommitMessageIssueConfig{Regex: "[A-Z]+-[0-9]+"},
}

var ccfgHash = CommitMessageConfig{
	Types: []string{"feat", "fix"},
	Scope: CommitMessageScopeConfig{},
	Footer: map[string]CommitMessageFooterConfig{
		"issue": {Key: "jira", KeySynonyms: []string{"Jira"}, UseHash: true},
		"refs":  {Key: "Refs", UseHash: true},
	},
	Issue: CommitMessageIssueConfig{Regex: "[A-Z]+-[0-9]+"},
}

var ccfgGitIssue = CommitMessageConfig{
	Types: []string{"feat", "fix"},
	Scope: CommitMessageScopeConfig{},
	Footer: map[string]CommitMessageFooterConfig{
		"issue": {Key: "issue", KeySynonyms: []string{"Issue"}, UseHash: false, AddValuePrefix: "#"},
	},
	Issue: CommitMessageIssueConfig{Regex: "#?[0-9]+"},
}

var ccfgEmptyIssue = CommitMessageConfig{
	Types: []string{"feat", "fix"},
	Scope: CommitMessageScopeConfig{},
	Footer: map[string]CommitMessageFooterConfig{
		"issue": {},
	},
	Issue: CommitMessageIssueConfig{Regex: "[A-Z]+-[0-9]+"},
}

var ccfgWithScope = CommitMessageConfig{
	Types: []string{"feat", "fix"},
	Scope: CommitMessageScopeConfig{Values: []string{"", "scope"}},
	Footer: map[string]CommitMessageFooterConfig{
		"issue": {Key: "jira", KeySynonyms: []string{"Jira"}},
		"refs":  {Key: "Refs", UseHash: true},
	},
	Issue: CommitMessageIssueConfig{Regex: "[A-Z]+-[0-9]+"},
}

func newBranchCfg(skipDetached bool) BranchesConfig {
	return BranchesConfig{
		Prefix:       "([a-z]+\\/)?",
		Suffix:       "(-.*)?",
		Skip:         []string{"develop", "master"},
		SkipDetached: &skipDetached,
	}
}

func newCommitMessageCfg(headerSelector string) CommitMessageConfig {
	return CommitMessageConfig{
		Types: []string{"feat", "fix"},
		Scope: CommitMessageScopeConfig{Values: []string{"", "scope"}},
		Footer: map[string]CommitMessageFooterConfig{
			"issue": {Key: "jira", KeySynonyms: []string{"Jira"}},
			"refs":  {Key: "Refs", UseHash: true},
		},
		Issue:          CommitMessageIssueConfig{Regex: "[A-Z]+-[0-9]+"},
		HeaderSelector: headerSelector,
	}
}

// messages samples start.
var fullMessage = `fix: correct minor typos in code

see the issue for details

on typos fixed.

Reviewed-by: Z
Refs #133`

var fullMessageWithJira = `fix: correct minor typos in code

see the issue for details

on typos fixed.

Reviewed-by: Z
Refs #133
jira: JIRA-456`

var fullMessageRefs = `fix: correct minor typos in code

see the issue for details

on typos fixed.

Refs #133`

var subjectAndBodyMessage = `fix: correct minor typos in code

see the issue for details

on typos fixed.`

var subjectAndFooterMessage = `refactor!: drop support for Node 6

BREAKING CHANGE: refactor to use JavaScript features not available in Node 6.`

// multiline samples end

func TestMessageProcessorImpl_SkipBranch(t *testing.T) {
	tests := []struct {
		name     string
		bcfg     BranchesConfig
		branch   string
		detached bool
		want     bool
	}{
		{"normal branch", newBranchCfg(false), "JIRA-123", false, false},
		{"dont ignore detached branch", newBranchCfg(false), "JIRA-123", true, false},
		{"ignore branch on skip list", newBranchCfg(false), "master", false, true},
		{"ignore detached branch", newBranchCfg(true), "JIRA-123", true, true},
		{"null skip detached", BranchesConfig{Skip: []string{}}, "JIRA-123", true, false},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			p := NewMessageProcessor(ccfg, tt.bcfg)
			if got := p.SkipBranch(tt.branch, tt.detached); got != tt.want {
				t.Errorf("MessageProcessorImpl.SkipBranch() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestMessageProcessorImpl_Validate(t *testing.T) {
	tests := []struct {
		name    string
		cfg     CommitMessageConfig
		message string
		wantErr bool
	}{
		{"single line valid message", ccfg, "feat: add something", false},
		{"single line valid message with scope", ccfg, "feat(scope): add something", false},
		{"single line valid scope from list", ccfgWithScope, "feat(scope): add something", false},
		{"single line invalid scope from list", ccfgWithScope, "feat(invalid): add something", true},
		{"single line invalid type message", ccfg, "something: add something", true},
		{"single line invalid type message", ccfg, "feat?: add something", true},

		{"multi line valid message", ccfg, `feat: add something
		
		team: x`, false},

		{"multi line invalid message", ccfg, `feat add something
		
		team: x`, true},

		{"support ! for breaking change", ccfg, "feat!: add something", false},
		{"support ! with scope for breaking change", ccfg, "feat(scope)!: add something", false},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			p := NewMessageProcessor(tt.cfg, newBranchCfg(false))
			if err := p.Validate(tt.message); (err != nil) != tt.wantErr {
				t.Errorf("MessageProcessorImpl.Validate() error = %v, wantErr %v", err, tt.wantErr)
			}
		})
	}
}

func TestMessageProcessorImpl_ValidateType(t *testing.T) {
	tests := []struct {
		name    string
		cfg     CommitMessageConfig
		ctype   string
		wantErr bool
	}{
		{"valid type", ccfg, "feat", false},
		{"invalid type", ccfg, "aaa", true},
		{"empty type", ccfg, "", true},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			p := NewMessageProcessor(tt.cfg, newBranchCfg(false))
			if err := p.ValidateType(tt.ctype); (err != nil) != tt.wantErr {
				t.Errorf("MessageProcessorImpl.ValidateType() error = %v, wantErr %v", err, tt.wantErr)
			}
		})
	}
}

func TestMessageProcessorImpl_ValidateScope(t *testing.T) {
	tests := []struct {
		name    string
		cfg     CommitMessageConfig
		scope   string
		wantErr bool
	}{
		{"any scope", ccfg, "aaa", false},
		{"valid scope with scope list", ccfgWithScope, "scope", false},
		{"invalid scope with scope list", ccfgWithScope, "aaa", true},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			p := NewMessageProcessor(tt.cfg, newBranchCfg(false))
			if err := p.ValidateScope(tt.scope); (err != nil) != tt.wantErr {
				t.Errorf("MessageProcessorImpl.ValidateScope() error = %v, wantErr %v", err, tt.wantErr)
			}
		})
	}
}

func TestMessageProcessorImpl_ValidateDescription(t *testing.T) {
	tests := []struct {
		name        string
		cfg         CommitMessageConfig
		description string
		wantErr     bool
	}{
		{"empty description", ccfg, "", true},
		{"sigle letter description", ccfg, "a", false},
		{"number description", ccfg, "1", true},
		{"valid description", ccfg, "add some feature", false},
		{"invalid capital letter description", ccfg, "Add some feature", true},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			p := NewMessageProcessor(tt.cfg, newBranchCfg(false))
			if err := p.ValidateDescription(tt.description); (err != nil) != tt.wantErr {
				t.Errorf("MessageProcessorImpl.ValidateDescription() error = %v, wantErr %v", err, tt.wantErr)
			}
		})
	}
}

func TestMessageProcessorImpl_Enhance(t *testing.T) {
	tests := []struct {
		name    string
		cfg     CommitMessageConfig
		branch  string
		message string
		want    string
		wantErr bool
	}{
		{"issue on branch name", ccfg, "JIRA-123", "fix: fix something", "\njira: JIRA-123", false},
		{"issue on branch name with description", ccfg, "JIRA-123-some-description", "fix: fix something", "\njira: JIRA-123", false},
		{"issue on branch name with prefix", ccfg, "feature/JIRA-123", "fix: fix something", "\njira: JIRA-123", false},
		{"with footer", ccfg, "JIRA-123", fullMessage, "jira: JIRA-123", false},
		{"with issue on footer", ccfg, "JIRA-123", fullMessageWithJira, "", false},
		{"issue on branch name with prefix and description", ccfg, "feature/JIRA-123-some-description", "fix: fix something", "\njira: JIRA-123", false},
		{"no issue on branch name", ccfg, "branch", "fix: fix something", "", true},
		{"unexpected branch name", ccfg, "feature /JIRA-123", "fix: fix something", "", true},
		{"issue on branch name using hash", ccfgHash, "JIRA-123-some-description", "fix: fix something", "\njira #JIRA-123", false},
		{"numeric issue on branch name", ccfgGitIssue, "#13", "fix: fix something", "\nissue: #13", false},
		{"numeric issue on branch name without hash", ccfgGitIssue, "13", "fix: fix something", "\nissue: #13", false},
		{"numeric issue on branch name with description without hash", ccfgGitIssue, "13-some-fix", "fix: fix something", "\nissue: #13", false},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := NewMessageProcessor(tt.cfg, newBranchCfg(false)).Enhance(tt.branch, tt.message)
			if (err != nil) != tt.wantErr {
				t.Errorf("MessageProcessorImpl.Enhance() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if got != tt.want {
				t.Errorf("MessageProcessorImpl.Enhance() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestMessageProcessorImpl_IssueID(t *testing.T) {
	p := NewMessageProcessor(ccfg, newBranchCfg(false))

	tests := []struct {
		name    string
		branch  string
		want    string
		wantErr bool
	}{
		{"simple branch", "JIRA-123", "JIRA-123", false},
		{"branch with prefix", "feature/JIRA-123", "JIRA-123", false},
		{"branch with prefix and posfix", "feature/JIRA-123-some-description", "JIRA-123", false},
		{"branch not found", "feature/wrong123-some-description", "", false},
		{"empty branch", "", "", false},
		{"unexpected branch name", "feature /JIRA-123", "", false},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := p.IssueID(tt.branch)
			if (err != nil) != tt.wantErr {
				t.Errorf("MessageProcessorImpl.IssueID() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if got != tt.want {
				t.Errorf("MessageProcessorImpl.IssueID() = %v, want %v", got, tt.want)
			}
		})
	}
}

const (
	multilineBody = `a
b
c`
	fullFooter = `BREAKING CHANGE: breaks
jira: JIRA-123`
)

func Test_hasIssueID(t *testing.T) {
	cfgColon := CommitMessageFooterConfig{Key: "jira"}
	cfgHash := CommitMessageFooterConfig{Key: "jira", UseHash: true}
	cfgEmpty := CommitMessageFooterConfig{}

	tests := []struct {
		name     string
		message  string
		issueCfg CommitMessageFooterConfig
		want     bool
	}{
		{"single line without issue", "feat: something", cfgColon, false},
		{"multi line without issue", `feat: something
		
yay`, cfgColon, false},
		{"multi line without jira issue", `feat: something
		
jira1: JIRA-123`, cfgColon, false},
		{"multi line with issue", `feat: something
		
jira: JIRA-123`, cfgColon, true},
		{"multi line with issue and hash", `feat: something
		
jira #JIRA-123`, cfgHash, true},
		{"empty config", `feat: something
		
jira #JIRA-123`, cfgEmpty, false},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := hasIssueID(tt.message, tt.issueCfg); got != tt.want {
				t.Errorf("hasIssueID() = %v, want %v", got, tt.want)
			}
		})
	}
}

func Test_hasFooter(t *testing.T) {
	tests := []struct {
		name    string
		message string
		want    bool
	}{
		{"simple message", "feat: add something", false},
		{"full messsage", fullMessage, true},
		{"full messsage with refs", fullMessageRefs, true},
		{"subject and footer message", subjectAndFooterMessage, true},
		{"subject and body message", subjectAndBodyMessage, false},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := hasFooter(tt.message); got != tt.want {
				t.Errorf("hasFooter() = %v, want %v", got, tt.want)
			}
		})
	}
}

// conventional commit tests

var completeBody = `some descriptions

jira: JIRA-123
BREAKING CHANGE: this change breaks everything`

var bodyWithCarriage = "some description\r\nmore description\r\n\r\njira: JIRA-123\r"
var expectedBodyWithCarriage = "some description\nmore description\n\njira: JIRA-123"

var issueOnlyBody = `some descriptions

jira: JIRA-456`

var issueSynonymsBody = `some descriptions

Jira: JIRA-789`

var hashMetadataBody = `some descriptions

Jira: JIRA-999
Refs #123`

func TestMessageProcessorImpl_Parse(t *testing.T) {
	tests := []struct {
		name    string
		cfg     CommitMessageConfig
		subject string
		body    string
		want    CommitMessage
	}{
		{"simple message", ccfg, "feat: something awesome", "", CommitMessage{Type: "feat", Scope: "", Description: "something awesome", Body: "", IsBreakingChange: false, Metadata: map[string]string{}}},
		{"message with scope", ccfg, "feat(scope): something awesome", "", CommitMessage{Type: "feat", Scope: "scope", Description: "something awesome", Body: "", IsBreakingChange: false, Metadata: map[string]string{}}},
		{"unmapped type", ccfg, "unkn: something unknown", "", CommitMessage{Type: "unkn", Scope: "", Description: "something unknown", Body: "", IsBreakingChange: false, Metadata: map[string]string{}}},
		{"jira and breaking change metadata", ccfg, "feat: something new", completeBody, CommitMessage{Type: "feat", Scope: "", Description: "something new", Body: completeBody, IsBreakingChange: true, Metadata: map[string]string{issueMetadataKey: "JIRA-123", breakingChangeMetadataKey: "this change breaks everything"}}},
		{"jira only metadata", ccfg, "feat: something new", issueOnlyBody, CommitMessage{Type: "feat", Scope: "", Description: "something new", Body: issueOnlyBody, IsBreakingChange: false, Metadata: map[string]string{issueMetadataKey: "JIRA-456"}}},
		{"jira synonyms metadata", ccfg, "feat: something new", issueSynonymsBody, CommitMessage{Type: "feat", Scope: "", Description: "something new", Body: issueSynonymsBody, IsBreakingChange: false, Metadata: map[string]string{issueMetadataKey: "JIRA-789"}}},
		{"breaking change with exclamation mark", ccfg, "feat!: something new", "", CommitMessage{Type: "feat", Scope: "", Description: "something new", Body: "", IsBreakingChange: true, Metadata: map[string]string{}}},
		{"hash metadata", ccfg, "feat: something new", hashMetadataBody, CommitMessage{Type: "feat", Scope: "", Description: "something new", Body: hashMetadataBody, IsBreakingChange: false, Metadata: map[string]string{issueMetadataKey: "JIRA-999", "refs": "#123"}}},
		{"empty issue cfg", ccfgEmptyIssue, "feat: something new", hashMetadataBody, CommitMessage{Type: "feat", Scope: "", Description: "something new", Body: hashMetadataBody, IsBreakingChange: false, Metadata: map[string]string{}}},
		{"carriage return on body", ccfg, "feat: something new", bodyWithCarriage, CommitMessage{Type: "feat", Scope: "", Description: "something new", Body: expectedBodyWithCarriage, IsBreakingChange: false, Metadata: map[string]string{issueMetadataKey: "JIRA-123"}}},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got, err := NewMessageProcessor(tt.cfg, newBranchCfg(false)).Parse(tt.subject, tt.body); !reflect.DeepEqual(got, tt.want) && err == nil {
				t.Errorf("MessageProcessorImpl.Parse() = [%+v], want [%+v]", got, tt.want)
			}
		})
	}
}

func TestMessageProcessorImpl_Format(t *testing.T) {
	tests := []struct {
		name       string
		cfg        CommitMessageConfig
		msg        CommitMessage
		wantHeader string
		wantBody   string
		wantFooter string
	}{
		{"simple message", ccfg, NewCommitMessage("feat", "", "something", "", "", ""), "feat: something", "", ""},
		{"with issue", ccfg, NewCommitMessage("feat", "", "something", "", "JIRA-123", ""), "feat: something", "", "jira: JIRA-123"},
		{"with issue using hash", ccfgHash, NewCommitMessage("feat", "", "something", "", "JIRA-123", ""), "feat: something", "", "jira #JIRA-123"},
		{"with issue using double hash", ccfgHash, NewCommitMessage("feat", "", "something", "", "#JIRA-123", ""), "feat: something", "", "jira #JIRA-123"},
		{"with breaking change", ccfg, NewCommitMessage("feat", "", "something", "", "", "breaks"), "feat: something", "", "BREAKING CHANGE: breaks"},
		{"with scope", ccfg, NewCommitMessage("feat", "scope", "something", "", "", ""), "feat(scope): something", "", ""},
		{"with body", ccfg, NewCommitMessage("feat", "", "something", "body", "", ""), "feat: something", "body", ""},
		{"with multiline body", ccfg, NewCommitMessage("feat", "", "something", multilineBody, "", ""), "feat: something", multilineBody, ""},
		{"full message", ccfg, NewCommitMessage("feat", "scope", "something", multilineBody, "JIRA-123", "breaks"), "feat(scope): something", multilineBody, fullFooter},
		{"config without issue key", ccfgEmptyIssue, NewCommitMessage("feat", "", "something", "", "JIRA-123", ""), "feat: something", "", ""},
		{"with issue and issue prefix", ccfgGitIssue, NewCommitMessage("feat", "", "something", "", "123", ""), "feat: something", "", "issue: #123"},
		{"with #issue and issue prefix", ccfgGitIssue, NewCommitMessage("feat", "", "something", "", "#123", ""), "feat: something", "", "issue: #123"},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, got1, got2 := NewMessageProcessor(tt.cfg, newBranchCfg(false)).Format(tt.msg)
			if got != tt.wantHeader {
				t.Errorf("MessageProcessorImpl.Format() header got = %v, want %v", got, tt.wantHeader)
			}
			if got1 != tt.wantBody {
				t.Errorf("MessageProcessorImpl.Format() body got = %v, want %v", got1, tt.wantBody)
			}
			if got2 != tt.wantFooter {
				t.Errorf("MessageProcessorImpl.Format() footer got = %v, want %v", got2, tt.wantFooter)
			}
		})
	}
}

var expectedBodyFullMessage = `
see the issue for details

on typos fixed.

Reviewed-by: Z
Refs #133`

func Test_splitCommitMessageContent(t *testing.T) {
	tests := []struct {
		name        string
		content     string
		wantSubject string
		wantBody    string
	}{
		{"single line commit", "feat: something", "feat: something", ""},
		{"multi line commit", fullMessage, "fix: correct minor typos in code", expectedBodyFullMessage},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, got1 := splitCommitMessageContent(tt.content)
			if got != tt.wantSubject {
				t.Errorf("splitCommitMessageContent() subject got = %v, want %v", got, tt.wantSubject)
			}
			if got1 != tt.wantBody {
				t.Errorf("splitCommitMessageContent() body got1 = [%v], want [%v]", got1, tt.wantBody)
			}
		})
	}
}

func Test_parseSubjectMessage(t *testing.T) {
	tests := []struct {
		name                  string
		message               string
		wantType              string
		wantScope             string
		wantDescription       string
		wantHasBreakingChange bool
	}{
		{"valid commit", "feat: something", "feat", "", "something", false},
		{"valid commit with scope", "feat(scope): something", "feat", "scope", "something", false},
		{"valid commit with breaking change", "feat(scope)!: something", "feat", "scope", "something", true},
		{"missing description", "feat: ", "feat", "", "", false},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctype, scope, description, hasBreakingChange := parseSubjectMessage(tt.message)
			if ctype != tt.wantType {
				t.Errorf("parseSubjectMessage() type got = %v, want %v", ctype, tt.wantType)
			}
			if scope != tt.wantScope {
				t.Errorf("parseSubjectMessage() scope got = %v, want %v", scope, tt.wantScope)
			}
			if description != tt.wantDescription {
				t.Errorf("parseSubjectMessage() description got = %v, want %v", description, tt.wantDescription)
			}
			if hasBreakingChange != tt.wantHasBreakingChange {
				t.Errorf("parseSubjectMessage() hasBreakingChange got = %v, want %v", hasBreakingChange, tt.wantHasBreakingChange)
			}
		})
	}
}

func Test_prepareHeader(t *testing.T) {
	tests := []struct {
		name           string
		headerSelector string
		commitHeader   string
		wantHeader     string
		wantError      bool
	}{
		{"conventional without selector", "", "feat: something", "feat: something", false},
		{"conventional with scope without selector", "", "feat(scope): something", "feat(scope): something", false},
		{"non-conventional without selector", "", "something", "something", false},
		{"matching conventional with selector with group", "Merged PR (\\d+): (?P<header>.*)", "Merged PR 123: feat: something", "feat: something", false},
		{"matching non-conventional with selector with group", "Merged PR (\\d+): (?P<header>.*)", "Merged PR 123: something", "something", false},
		{"matching non-conventional with selector without group", "Merged PR (\\d+): (.*)", "Merged PR 123: something", "", true},
		{"non-matching non-conventional with selector with group", "Merged PR (\\d+): (?P<header>.*)", "something", "", true},
		{"matching non-conventional with invalid regex", "Merged PR (\\d+): (?<header>.*)", "Merged PR 123: something", "", true},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			msgProcessor := NewMessageProcessor(newCommitMessageCfg(tt.headerSelector), newBranchCfg(false))
			header, err := msgProcessor.prepareHeader(tt.commitHeader)

			if tt.wantError && err == nil {
				t.Errorf("prepareHeader() err got = %v, want not nil", err)
			}
			if header != tt.wantHeader {
				t.Errorf("prepareHeader() header got = %v, want %v", header, tt.wantHeader)
			}
		})
	}
}

func Test_removeCarriage(t *testing.T) {
	tests := []struct {
		name   string
		commit string
		want   string
	}{
		{"normal string", "normal string", "normal string"},
		{"break line", "normal\nstring", "normal\nstring"},
		{"carriage return", "normal\r\nstring", "normal\nstring"},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := removeCarriage(tt.commit); got != tt.want {
				t.Errorf("removeCarriage() = %v, want %v", got, tt.want)
			}
		})
	}
}
