package workflow

import (
	"encoding/json"
	"strings"
	"testing"

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

var trigger = TriggerMetadata{
	Kind: string(TriggerKindPush),
	Push: &PushTriggerData{
		Ref:    "refs/heads/main",
		OldSha: strings.Repeat("0", 40),
		NewSha: strings.Repeat("f", 40),
	},
}

var when = []Constraint{
	{
		Event:  []string{"push"},
		Branch: []string{"main"},
	},
}

func TestCompileWorkflow_MatchingWorkflowWithSteps(t *testing.T) {
	wf := Workflow{
		Name:      ".tangled/workflows/test.yml",
		Engine:    "nixery",
		When:      when,
		CloneOpts: CloneOpts{}, // default true
	}

	c := Compiler{Trigger: trigger}
	cp := c.Compile([]Workflow{wf})

	assert.Len(t, cp.Workflows, 1)
	assert.Equal(t, wf.Name, cp.Workflows[0].Name)
	assert.False(t, cp.Workflows[0].Clone.Skip)
	assert.False(t, c.Diagnostics.IsErr())
}

func TestCompileWorkflow_RunsOnLabelsPersistWithoutFanout(t *testing.T) {
	wf := Workflow{
		Name:   ".tangled/workflows/arm64.yml",
		Engine: "microvm",
		When:   when,
		RunsOn: []string{"linux/arm64", "kvm"},
	}

	c := Compiler{Trigger: trigger}
	cp := c.Compile([]Workflow{wf})

	assert.Len(t, cp.Workflows, 1)
	assert.Equal(t, []string{"linux/arm64", "kvm"}, cp.Workflows[0].RunsOn)

	raw, err := json.Marshal(cp.Workflows[0])
	assert.NoError(t, err)

	var persisted map[string]any
	assert.NoError(t, json.Unmarshal(raw, &persisted))
	assert.Equal(t, []any{"linux/arm64", "kvm"}, persisted["runsOn"])
}

func TestCompileWorkflow_AudienceReachesTheCompiledRecord(t *testing.T) {
	wf := Workflow{
		Name:     ".tangled/workflows/deploy.yml",
		Engine:   "nixery",
		When:     when,
		Audience: "//iam.googleapis.com/projects/tangled/locations/global/workloadIdentityPools/ci/providers/spindle",
	}

	c := Compiler{Trigger: trigger}
	cp := c.Compile([]Workflow{wf})

	assert.Len(t, cp.Workflows, 1)
	assert.Equal(t, wf.Audience, *cp.Workflows[0].Audience)

	raw, err := json.Marshal(cp.Workflows[0])
	assert.NoError(t, err)

	var persisted map[string]any
	assert.NoError(t, json.Unmarshal(raw, &persisted))
	assert.Equal(t, wf.Audience, persisted["audience"])
}

func TestCompileWorkflow_WithoutAudienceOmitsTheField(t *testing.T) {
	wf := Workflow{
		Name:   ".tangled/workflows/build.yml",
		Engine: "nixery",
		When:   when,
	}

	c := Compiler{Trigger: trigger}
	cp := c.Compile([]Workflow{wf})

	assert.Len(t, cp.Workflows, 1)
	assert.Nil(t, cp.Workflows[0].Audience)

	raw, err := json.Marshal(cp.Workflows[0])
	assert.NoError(t, err)

	var persisted map[string]any
	assert.NoError(t, json.Unmarshal(raw, &persisted))
	assert.NotContains(t, persisted, "audience")
}

func TestCompileWorkflow_LegacyWorkflowOmitsRunsOn(t *testing.T) {
	wf := Workflow{
		Name:   ".tangled/workflows/legacy.yml",
		Engine: "microvm",
		When:   when,
	}

	c := Compiler{Trigger: trigger}
	cp := c.Compile([]Workflow{wf})

	assert.Len(t, cp.Workflows, 1)
	assert.Empty(t, cp.Workflows[0].RunsOn)

	raw, err := json.Marshal(cp.Workflows[0])
	assert.NoError(t, err)

	var persisted map[string]any
	assert.NoError(t, json.Unmarshal(raw, &persisted))
	_, ok := persisted["runsOn"]
	assert.False(t, ok, "legacy workflow JSON should not gain runsOn")
}

func TestCompileWorkflow_TriggerMismatch(t *testing.T) {
	wf := Workflow{
		Name:   ".tangled/workflows/mismatch.yml",
		Engine: "nixery",
		When: []Constraint{
			{
				Event:  []string{"push"},
				Branch: []string{"master"}, // different branch
			},
		},
	}

	c := Compiler{Trigger: trigger}
	cp := c.Compile([]Workflow{wf})

	assert.Len(t, cp.Workflows, 0)
	assert.Len(t, c.Diagnostics.Warnings, 1)
	assert.Equal(t, WorkflowSkipped, c.Diagnostics.Warnings[0].Type)
}

func TestCompileWorkflow_CloneFalseWithShallowTrue(t *testing.T) {
	wf := Workflow{
		Name:   ".tangled/workflows/clone_skip.yml",
		Engine: "nixery",
		When:   when,
		CloneOpts: CloneOpts{
			Skip:  true,
			Depth: 1,
		}, // false
	}

	c := Compiler{Trigger: trigger}
	cp := c.Compile([]Workflow{wf})

	assert.Len(t, cp.Workflows, 1)
	assert.True(t, cp.Workflows[0].Clone.Skip)
	assert.Len(t, c.Diagnostics.Warnings, 1)
	assert.Equal(t, InvalidConfiguration, c.Diagnostics.Warnings[0].Type)
}

func TestCompileWorkflow_MissingEngine(t *testing.T) {
	wf := Workflow{
		Name:   ".tangled/workflows/missing_engine.yml",
		When:   when,
		Engine: "",
	}

	c := Compiler{Trigger: trigger}
	cp := c.Compile([]Workflow{wf})

	assert.Len(t, cp.Workflows, 0)
	assert.Len(t, c.Diagnostics.Errors, 1)
	assert.Equal(t, MissingEngine, c.Diagnostics.Errors[0].Error)
}

func TestCompileWorkflow_ChangedFilesMatchesPaths(t *testing.T) {
	wf := Workflow{
		Name:   ".tangled/workflows/test.yml",
		Engine: "nixery",
		When: []Constraint{
			{
				Event:  []string{"push"},
				Branch: []string{"main"},
				Paths:  []string{"src/**"},
			},
		},
	}

	c := Compiler{
		Trigger:      trigger,
		ChangedFiles: []string{"src/main.go", "src/util.go"},
	}
	cp := c.Compile([]Workflow{wf})

	assert.Len(t, cp.Workflows, 1)
	assert.Equal(t, wf.Name, cp.Workflows[0].Name)
	assert.False(t, c.Diagnostics.IsErr())
}

func TestCompileWorkflow_ChangedFilesNoMatch(t *testing.T) {
	wf := Workflow{
		Name:   ".tangled/workflows/test.yml",
		Engine: "nixery",
		When: []Constraint{
			{
				Event:  []string{"push"},
				Branch: []string{"main"},
				Paths:  []string{"src/**"},
			},
		},
	}

	c := Compiler{
		Trigger:      trigger,
		ChangedFiles: []string{"docs/guide.md", "README.md"},
	}
	cp := c.Compile([]Workflow{wf})

	assert.Len(t, cp.Workflows, 0)
	assert.Len(t, c.Diagnostics.Warnings, 1)
	assert.Equal(t, WorkflowSkipped, c.Diagnostics.Warnings[0].Type)
}

func TestCompileWorkflow_NoPaths_ChangedFilesIgnored(t *testing.T) {
	wf := Workflow{
		Name:   ".tangled/workflows/test.yml",
		Engine: "nixery",
		When:   when, // no Paths constraint
	}

	c := Compiler{
		Trigger:      trigger,
		ChangedFiles: []string{"docs/guide.md"},
	}
	cp := c.Compile([]Workflow{wf})

	assert.Len(t, cp.Workflows, 1)
	assert.False(t, c.Diagnostics.IsErr())
}

func TestCompileWorkflow_MultipleBranchAndTag(t *testing.T) {
	wf := Workflow{
		Name: ".tangled/workflows/branch_and_tag.yml",
		When: []Constraint{
			{
				Event:  []string{"push"},
				Branch: []string{"main", "develop"},
				Tag:    []string{"v*"},
			},
		},
		Engine: "nixery",
	}

	tests := []struct {
		name          string
		trigger       TriggerMetadata
		shouldMatch   bool
		expectedCount int
	}{
		{
			name: "matches main branch",
			trigger: TriggerMetadata{
				Kind: string(TriggerKindPush),
				Push: &PushTriggerData{
					Ref:    "refs/heads/main",
					OldSha: strings.Repeat("0", 40),
					NewSha: strings.Repeat("f", 40),
				},
			},
			shouldMatch:   true,
			expectedCount: 1,
		},
		{
			name: "matches develop branch",
			trigger: TriggerMetadata{
				Kind: string(TriggerKindPush),
				Push: &PushTriggerData{
					Ref:    "refs/heads/develop",
					OldSha: strings.Repeat("0", 40),
					NewSha: strings.Repeat("f", 40),
				},
			},
			shouldMatch:   true,
			expectedCount: 1,
		},
		{
			name: "matches v* tag pattern",
			trigger: TriggerMetadata{
				Kind: string(TriggerKindPush),
				Push: &PushTriggerData{
					Ref:    "refs/tags/v1.0.0",
					OldSha: strings.Repeat("0", 40),
					NewSha: strings.Repeat("f", 40),
				},
			},
			shouldMatch:   true,
			expectedCount: 1,
		},
		{
			name: "matches v* tag pattern with different version",
			trigger: TriggerMetadata{
				Kind: string(TriggerKindPush),
				Push: &PushTriggerData{
					Ref:    "refs/tags/v2.5.3",
					OldSha: strings.Repeat("0", 40),
					NewSha: strings.Repeat("f", 40),
				},
			},
			shouldMatch:   true,
			expectedCount: 1,
		},
		{
			name: "does not match master branch",
			trigger: TriggerMetadata{
				Kind: string(TriggerKindPush),
				Push: &PushTriggerData{
					Ref:    "refs/heads/master",
					OldSha: strings.Repeat("0", 40),
					NewSha: strings.Repeat("f", 40),
				},
			},
			shouldMatch:   false,
			expectedCount: 0,
		},
		{
			name: "does not match non-v tag",
			trigger: TriggerMetadata{
				Kind: string(TriggerKindPush),
				Push: &PushTriggerData{
					Ref:    "refs/tags/release-1.0",
					OldSha: strings.Repeat("0", 40),
					NewSha: strings.Repeat("f", 40),
				},
			},
			shouldMatch:   false,
			expectedCount: 0,
		},
		{
			name: "does not match feature branch",
			trigger: TriggerMetadata{
				Kind: string(TriggerKindPush),
				Push: &PushTriggerData{
					Ref:    "refs/heads/feature/new-feature",
					OldSha: strings.Repeat("0", 40),
					NewSha: strings.Repeat("f", 40),
				},
			},
			shouldMatch:   false,
			expectedCount: 0,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			c := Compiler{Trigger: tt.trigger}
			cp := c.Compile([]Workflow{wf})

			assert.Len(t, cp.Workflows, tt.expectedCount)
			if tt.shouldMatch {
				assert.Equal(t, wf.Name, cp.Workflows[0].Name)
			}
		})
	}
}
