// Copyright 2023 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package compiler

import (
	"testing"

	"github.com/stretchr/testify/assert"

	backend_types "go.woodpecker-ci.org/woodpecker/v3/pipeline/backend/types"
	"go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/yaml/constraint"
)

func TestConvertDAGToStages(t *testing.T) {
	steps := map[string]*dagCompilerStep{
		"step1": {
			step:      &backend_types.Step{},
			dependsOn: constraint.DependsOn{{Name: "step3"}},
		},
		"step2": {
			step:      &backend_types.Step{},
			dependsOn: constraint.DependsOn{{Name: "step1"}},
		},
		"step3": {
			step:      &backend_types.Step{},
			dependsOn: constraint.DependsOn{{Name: "step2"}},
		},
	}
	_, err := convertDAGToStages(steps)
	assert.ErrorIs(t, err, &ErrStepDependencyCycle{})

	steps = map[string]*dagCompilerStep{
		"step1": {
			step:      &backend_types.Step{},
			dependsOn: constraint.DependsOn{{Name: "step2"}},
		},
		"step2": {
			step: &backend_types.Step{},
		},
	}
	_, err = convertDAGToStages(steps)
	assert.NoError(t, err)

	steps = map[string]*dagCompilerStep{
		"a": {
			step: &backend_types.Step{},
		},
		"b": {
			step:      &backend_types.Step{},
			dependsOn: constraint.DependsOn{{Name: "a"}},
		},
		"c": {
			step:      &backend_types.Step{},
			dependsOn: constraint.DependsOn{{Name: "a"}},
		},
		"d": {
			step:      &backend_types.Step{},
			dependsOn: constraint.DependsOn{{Name: "b"}, {Name: "c"}},
		},
	}
	_, err = convertDAGToStages(steps)
	assert.NoError(t, err)

	steps = map[string]*dagCompilerStep{
		"step1": {
			step:      &backend_types.Step{},
			dependsOn: constraint.DependsOn{{Name: "not-existing-step"}},
		},
	}
	_, err = convertDAGToStages(steps)
	assert.ErrorIs(t, err, &ErrStepMissingDependency{})

	steps = map[string]*dagCompilerStep{
		"echo env": {
			position: 0,
			name:     "echo env",
			step: &backend_types.Step{
				UUID:  "01HJDPEW6R7J0JBE3F1T7Q0TYX",
				Type:  "commands",
				Name:  "echo env",
				Image: "bash",
			},
		},
		"echo 1": {
			position:  1,
			name:      "echo 1",
			dependsOn: constraint.DependsOn{{Name: "echo env"}, {Name: "echo 2"}},
			step: &backend_types.Step{
				UUID:  "01HJDPF770QGRZER8RF79XVS4M",
				Type:  "commands",
				Name:  "echo 1",
				Image: "bash",
			},
		},
		"echo 2": {
			position: 2,
			name:     "echo 2",
			step: &backend_types.Step{
				UUID:  "01HJDPFF5RMEYZW0YTGR1Y1ZR0",
				Type:  "commands",
				Name:  "echo 2",
				Image: "bash",
			},
		},
	}
	stages, err := convertDAGToStages(steps)
	assert.NoError(t, err)
	assert.EqualValues(t, []*backend_types.Stage{{
		Steps: []*backend_types.Step{{
			UUID:  "01HJDPEW6R7J0JBE3F1T7Q0TYX",
			Type:  "commands",
			Name:  "echo env",
			Image: "bash",
		}, {
			UUID:  "01HJDPFF5RMEYZW0YTGR1Y1ZR0",
			Type:  "commands",
			Name:  "echo 2",
			Image: "bash",
		}},
	}, {
		Steps: []*backend_types.Step{{
			UUID:  "01HJDPF770QGRZER8RF79XVS4M",
			Type:  "commands",
			Name:  "echo 1",
			Image: "bash",
		}},
	}}, stages)
}

func TestOptionalStepDependency(t *testing.T) {
	t.Run("missing optional step dep is dropped", func(t *testing.T) {
		steps := map[string]*dagCompilerStep{
			"build": {
				position: 0,
				name:     "build",
				step:     &backend_types.Step{Name: "build"},
			},
			"deploy": {
				position: 1,
				name:     "deploy",
				step:     &backend_types.Step{Name: "deploy"},
				dependsOn: constraint.DependsOn{
					{Name: "build"},
					{Name: "lint", Optional: true},
				},
			},
		}
		stages, err := convertDAGToStages(steps)
		assert.NoError(t, err)
		assert.Len(t, stages, 2, "should produce 2 stages (build then deploy)")
	})

	t.Run("missing required step dep still errors", func(t *testing.T) {
		steps := map[string]*dagCompilerStep{
			"deploy": {
				name: "deploy",
				step: &backend_types.Step{Name: "deploy"},
				dependsOn: constraint.DependsOn{
					{Name: "build"},
				},
			},
		}
		_, err := convertDAGToStages(steps)
		assert.ErrorIs(t, err, &ErrStepMissingDependency{})
	})

	t.Run("present optional step dep is kept", func(t *testing.T) {
		steps := map[string]*dagCompilerStep{
			"build": {
				position: 0,
				name:     "build",
				step:     &backend_types.Step{Name: "build"},
			},
			"lint": {
				position: 1,
				name:     "lint",
				step:     &backend_types.Step{Name: "lint"},
			},
			"deploy": {
				position: 2,
				name:     "deploy",
				step:     &backend_types.Step{Name: "deploy"},
				dependsOn: constraint.DependsOn{
					{Name: "build"},
					{Name: "lint", Optional: true},
				},
			},
		}
		stages, err := convertDAGToStages(steps)
		assert.NoError(t, err)
		assert.Len(t, stages, 2, "build+lint in stage 1, deploy in stage 2")
		assert.Len(t, stages[0].Steps, 2)
		assert.Len(t, stages[1].Steps, 1)
		assert.Equal(t, "deploy", stages[1].Steps[0].Name)
	})
}

func TestIsDag(t *testing.T) {
	steps := []*dagCompilerStep{
		{
			step: &backend_types.Step{},
		},
	}
	c := newDAGCompiler(steps)
	assert.False(t, c.isDAG())

	steps = []*dagCompilerStep{
		{
			step:      &backend_types.Step{},
			dependsOn: constraint.DependsOn{},
		},
	}
	c = newDAGCompiler(steps)
	assert.True(t, c.isDAG())
}
