package workflow

import (
	"testing"

	"github.com/stretchr/testify/assert"
	"tangled.org/core/api/tangled"
)

func TestUnmarshalWorkflowWithBranch(t *testing.T) {
	yamlData := `
when:
  - event: ["push", "pull_request"]
    branch: ["main", "develop"]`

	wf, err := FromFile("test.yml", []byte(yamlData))
	assert.NoError(t, err, "YAML should unmarshal without error")

	assert.Len(t, wf.When, 1, "Should have one constraint")
	assert.ElementsMatch(t, []string{"main", "develop"}, wf.When[0].Branch)
	assert.ElementsMatch(t, []string{"push", "pull_request"}, wf.When[0].Event)

	assert.False(t, wf.CloneOpts.Skip, "Skip should default to false")
}

func TestUnmarshalCloneFalse(t *testing.T) {
	yamlData := `
when:
  - event: pull_request_close

clone:
  skip: true
`

	wf, err := FromFile("test.yml", []byte(yamlData))
	assert.NoError(t, err)

	assert.ElementsMatch(t, []string{"pull_request_close"}, wf.When[0].Event)

	assert.True(t, wf.CloneOpts.Skip, "Skip should be false")
}

func TestUnmarshalWorkflowWithTags(t *testing.T) {
	yamlData := `
when:
  - event: ["push"]
    tag: ["v*", "release-*"]`

	wf, err := FromFile("test.yml", []byte(yamlData))
	assert.NoError(t, err, "YAML should unmarshal without error")

	assert.Len(t, wf.When, 1, "Should have one constraint")
	assert.ElementsMatch(t, []string{"v*", "release-*"}, wf.When[0].Tag)
	assert.ElementsMatch(t, []string{"push"}, wf.When[0].Event)
}

func TestUnmarshalWorkflowWithBranchAndTag(t *testing.T) {
	yamlData := `
when:
  - event: ["push"]
    branch: ["main", "develop"]
    tag: ["v*"]`

	wf, err := FromFile("test.yml", []byte(yamlData))
	assert.NoError(t, err, "YAML should unmarshal without error")

	assert.Len(t, wf.When, 1, "Should have one constraint")
	assert.ElementsMatch(t, []string{"main", "develop"}, wf.When[0].Branch)
	assert.ElementsMatch(t, []string{"v*"}, wf.When[0].Tag)
}

func TestMatchesPattern(t *testing.T) {
	tests := []struct {
		name     string
		input    string
		patterns []string
		expected bool
	}{
		{"exact match", "main", []string{"main"}, true},
		{"exact match in list", "develop", []string{"main", "develop"}, true},
		{"no match", "feature", []string{"main", "develop"}, false},
		{"wildcard prefix", "v1.0.0", []string{"v*"}, true},
		{"wildcard suffix", "release-1.0", []string{"*-1.0"}, true},
		{"wildcard middle", "feature-123-test", []string{"feature-*-test"}, true},
		{"double star prefix", "release-1.0.0", []string{"release-**"}, true},
		{"double star with slashes", "release/1.0/hotfix", []string{"release/**"}, true},
		{"double star matches multiple levels", "foo/bar/baz/qux", []string{"foo/**"}, true},
		{"double star no match", "feature/test", []string{"release/**"}, false},
		{"no patterns matches nothing", "anything", []string{}, false},
		{"pattern doesn't match", "v1.0.0", []string{"release-*"}, false},
		{"complex pattern", "release/v1.2.3", []string{"release/*"}, true},
		{"single star stops at slash", "release/1.0/hotfix", []string{"release/*"}, false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result, _ := matchesPattern(tt.input, tt.patterns)
			assert.Equal(t, tt.expected, result, "matchesPattern(%q, %v) should be %v", tt.input, tt.patterns, tt.expected)
		})
	}
}

func TestConstraintMatchRef_Branches(t *testing.T) {
	tests := []struct {
		name       string
		constraint Constraint
		ref        string
		expected   bool
	}{
		{
			name:       "exact branch match",
			constraint: Constraint{Branch: []string{"main"}},
			ref:        "refs/heads/main",
			expected:   true,
		},
		{
			name:       "branch glob match",
			constraint: Constraint{Branch: []string{"feature-*"}},
			ref:        "refs/heads/feature-123",
			expected:   true,
		},
		{
			name:       "branch no match",
			constraint: Constraint{Branch: []string{"main"}},
			ref:        "refs/heads/develop",
			expected:   false,
		},
		{
			name:       "no constraints matches nothing",
			constraint: Constraint{},
			ref:        "refs/heads/anything",
			expected:   false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result, _ := tt.constraint.MatchRef(tt.ref)
			assert.Equal(t, tt.expected, result, "MatchRef should return %v for ref %q", tt.expected, tt.ref)
		})
	}
}

func TestConstraintMatchRef_Tags(t *testing.T) {
	tests := []struct {
		name       string
		constraint Constraint
		ref        string
		expected   bool
	}{
		{
			name:       "exact tag match",
			constraint: Constraint{Tag: []string{"v1.0.0"}},
			ref:        "refs/tags/v1.0.0",
			expected:   true,
		},
		{
			name:       "tag glob match",
			constraint: Constraint{Tag: []string{"v*"}},
			ref:        "refs/tags/v1.2.3",
			expected:   true,
		},
		{
			name:       "tag glob with pattern",
			constraint: Constraint{Tag: []string{"release-*"}},
			ref:        "refs/tags/release-2024",
			expected:   true,
		},
		{
			name:       "tag no match",
			constraint: Constraint{Tag: []string{"v*"}},
			ref:        "refs/tags/release-1.0",
			expected:   false,
		},
		{
			name:       "tag not matched when only branch constraint",
			constraint: Constraint{Branch: []string{"main"}},
			ref:        "refs/tags/v1.0.0",
			expected:   false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result, _ := tt.constraint.MatchRef(tt.ref)
			assert.Equal(t, tt.expected, result, "MatchRef should return %v for ref %q", tt.expected, tt.ref)
		})
	}
}

func TestConstraintMatchRef_Combined(t *testing.T) {
	tests := []struct {
		name       string
		constraint Constraint
		ref        string
		expected   bool
	}{
		{
			name:       "matches branch in combined constraint",
			constraint: Constraint{Branch: []string{"main"}, Tag: []string{"v*"}},
			ref:        "refs/heads/main",
			expected:   true,
		},
		{
			name:       "matches tag in combined constraint",
			constraint: Constraint{Branch: []string{"main"}, Tag: []string{"v*"}},
			ref:        "refs/tags/v1.0.0",
			expected:   true,
		},
		{
			name:       "no match in combined constraint",
			constraint: Constraint{Branch: []string{"main"}, Tag: []string{"v*"}},
			ref:        "refs/heads/develop",
			expected:   false,
		},
		{
			name:       "glob patterns in combined constraint - branch",
			constraint: Constraint{Branch: []string{"release-*"}, Tag: []string{"v*"}},
			ref:        "refs/heads/release-2024",
			expected:   true,
		},
		{
			name:       "glob patterns in combined constraint - tag",
			constraint: Constraint{Branch: []string{"release-*"}, Tag: []string{"v*"}},
			ref:        "refs/tags/v2.0.0",
			expected:   true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result, _ := tt.constraint.MatchRef(tt.ref)
			assert.Equal(t, tt.expected, result, "MatchRef should return %v for ref %q", tt.expected, tt.ref)
		})
	}
}

func TestConstraintMatchBranch_GlobPatterns(t *testing.T) {
	tests := []struct {
		name       string
		constraint Constraint
		branch     string
		expected   bool
	}{
		{
			name:       "exact match",
			constraint: Constraint{Branch: []string{"main"}},
			branch:     "main",
			expected:   true,
		},
		{
			name:       "glob match",
			constraint: Constraint{Branch: []string{"feature-*"}},
			branch:     "feature-123",
			expected:   true,
		},
		{
			name:       "no match",
			constraint: Constraint{Branch: []string{"main"}},
			branch:     "develop",
			expected:   false,
		},
		{
			name:       "multiple patterns with match",
			constraint: Constraint{Branch: []string{"main", "release-*"}},
			branch:     "release-1.0",
			expected:   true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result, _ := tt.constraint.MatchBranch(tt.branch)
			assert.Equal(t, tt.expected, result, "MatchBranch should return %v for branch %q", tt.expected, tt.branch)
		})
	}
}

func TestMatchesAnyFile(t *testing.T) {
	tests := []struct {
		name     string
		files    []string
		patterns []string
		expected bool
	}{
		{
			name:     "exact file match",
			files:    []string{"src/main.go"},
			patterns: []string{"src/main.go"},
			expected: true,
		},
		{
			name:     "glob match single star",
			files:    []string{"src/main.go"},
			patterns: []string{"src/*.go"},
			expected: true,
		},
		{
			name:     "glob match double star",
			files:    []string{"src/pkg/util.go"},
			patterns: []string{"src/**/*.go"},
			expected: true,
		},
		{
			name:     "any file in list matches",
			files:    []string{"README.md", "src/main.go", "docs/guide.md"},
			patterns: []string{"src/**"},
			expected: true,
		},
		{
			name:     "no file matches",
			files:    []string{"README.md", "docs/guide.md"},
			patterns: []string{"src/**"},
			expected: false,
		},
		{
			name:     "empty files list",
			files:    []string{},
			patterns: []string{"src/**"},
			expected: false,
		},
		{
			name:     "nil files list",
			files:    nil,
			patterns: []string{"src/**"},
			expected: false,
		},
		{
			name:     "multiple patterns, second matches",
			files:    []string{"docs/guide.md"},
			patterns: []string{"src/**", "docs/**"},
			expected: true,
		},
		{
			name:     "single star does not cross directory boundary",
			files:    []string{"src/pkg/util.go"},
			patterns: []string{"src/*.go"},
			expected: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result, err := matchesAnyFile(tt.files, tt.patterns)
			assert.NoError(t, err)
			assert.Equal(t, tt.expected, result)
		})
	}
}

func TestConstraintMatch_PathsFilter(t *testing.T) {
	pushTrigger := tangled.Pipeline_TriggerMetadata{
		Kind: string(TriggerKindPush),
		Push: &tangled.Pipeline_PushTriggerData{
			Ref: "refs/heads/main",
		},
	}

	tests := []struct {
		name         string
		constraint   Constraint
		changedFiles []string
		expected     bool
	}{
		{
			name: "paths match - workflow runs",
			constraint: Constraint{
				Event:  []string{"push"},
				Branch: []string{"main"},
				Paths:  []string{"src/**"},
			},
			changedFiles: []string{"src/main.go"},
			expected:     true,
		},
		{
			name: "paths no match - workflow skipped",
			constraint: Constraint{
				Event:  []string{"push"},
				Branch: []string{"main"},
				Paths:  []string{"src/**"},
			},
			changedFiles: []string{"docs/guide.md"},
			expected:     false,
		},
		{
			name: "no paths filter - all files pass",
			constraint: Constraint{
				Event:  []string{"push"},
				Branch: []string{"main"},
			},
			changedFiles: []string{"docs/guide.md"},
			expected:     true,
		},
		{
			name: "paths filter with empty changed files - skipped",
			constraint: Constraint{
				Event:  []string{"push"},
				Branch: []string{"main"},
				Paths:  []string{"src/**"},
			},
			changedFiles: []string{},
			expected:     false,
		},
		{
			name: "paths glob matches one of many changed files",
			constraint: Constraint{
				Event:  []string{"push"},
				Branch: []string{"main"},
				Paths:  []string{"**/*.go"},
			},
			changedFiles: []string{"README.md", "go.mod", "src/main.go"},
			expected:     true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result, err := tt.constraint.Match(pushTrigger, tt.changedFiles)
			assert.NoError(t, err)
			assert.Equal(t, tt.expected, result)
		})
	}
}

func TestUnmarshalWorkflowWithPaths(t *testing.T) {
	yamlData := `
when:
  - event: push
    branch: main
    paths:
      - "src/**"
      - "**.go"`

	wf, err := FromFile("test.yml", []byte(yamlData))
	assert.NoError(t, err)
	assert.Len(t, wf.When, 1)
	assert.ElementsMatch(t, []string{"src/**", "**.go"}, wf.When[0].Paths)
}

func TestUnmarshalWorkflowWithPathsSingleString(t *testing.T) {
	yamlData := `
when:
  - event: push
    branch: main
    paths: "src/**"`

	wf, err := FromFile("test.yml", []byte(yamlData))
	assert.NoError(t, err)
	assert.Len(t, wf.When, 1)
	assert.ElementsMatch(t, []string{"src/**"}, wf.When[0].Paths)
}

func TestConstraintMatchTag_GlobPatterns(t *testing.T) {
	tests := []struct {
		name       string
		constraint Constraint
		tag        string
		expected   bool
	}{
		{
			name:       "exact match",
			constraint: Constraint{Tag: []string{"v1.0.0"}},
			tag:        "v1.0.0",
			expected:   true,
		},
		{
			name:       "glob match",
			constraint: Constraint{Tag: []string{"v*"}},
			tag:        "v2.3.4",
			expected:   true,
		},
		{
			name:       "no match",
			constraint: Constraint{Tag: []string{"v*"}},
			tag:        "release-1.0",
			expected:   false,
		},
		{
			name:       "multiple patterns with match",
			constraint: Constraint{Tag: []string{"v*", "release-*"}},
			tag:        "release-2024",
			expected:   true,
		},
		{
			name:       "empty tag list matches nothing",
			constraint: Constraint{Tag: []string{}},
			tag:        "v1.0.0",
			expected:   false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result, _ := tt.constraint.MatchTag(tt.tag)
			assert.Equal(t, tt.expected, result, "MatchTag should return %v for tag %q", tt.expected, tt.tag)
		})
	}
}

func TestMatch_ManualDispatch(t *testing.T) {
	// manual dispatch is policy-free: every workflow matches regardless of its
	// declared event/branch/tag/path constraints. Selection is the caller's job.
	manualTrigger := tangled.Pipeline_TriggerMetadata{
		Kind:   string(TriggerKindManual),
		Manual: &tangled.Pipeline_ManualTriggerData{Sha: "deadbeef"},
	}

	workflows := []Workflow{
		{When: nil},
		{When: []Constraint{{Event: []string{"push"}, Branch: []string{"main"}}}},
		{When: []Constraint{{Event: []string{"pull_request"}, Paths: []string{"src/**"}}}},
		{When: []Constraint{{Event: []string{"push"}, Tag: []string{"v*"}}}},
	}

	for i, wf := range workflows {
		result, err := wf.Match(manualTrigger, nil)
		assert.NoError(t, err)
		assert.True(t, result, "workflow %d should match a manual dispatch", i)
	}
}
