// 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 yaml

import (
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"gopkg.in/yaml.v3"

	"go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/metadata"
	yaml_base_types "go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/yaml/types/base"
)

func TestParse(t *testing.T) {
	t.Run("Should unmarshal a string", func(t *testing.T) {
		out, err := ParseString(sampleYaml)
		assert.NoError(t, err)

		assert.Contains(t, out.When.Constraints[0].Event, "tester")

		assert.Equal(t, "/go", out.Workspace.Base)
		assert.Equal(t, "src/github.com/octocat/hello-world", out.Workspace.Path)
		assert.Equal(t, "database", out.Services.ContainerList[0].Name)
		assert.Equal(t, "mysql", out.Services.ContainerList[0].Image)
		assert.Equal(t, "test", out.Steps.ContainerList[0].Name)
		assert.Equal(t, "golang", out.Steps.ContainerList[0].Image)
		assert.Equal(t, yaml_base_types.StringOrSlice{"go install", "go test"}, out.Steps.ContainerList[0].Commands)
		assert.Equal(t, "build", out.Steps.ContainerList[1].Name)
		assert.Equal(t, "golang", out.Steps.ContainerList[1].Image)
		assert.Equal(t, yaml_base_types.StringOrSlice{"go build"}, out.Steps.ContainerList[1].Commands)
		assert.Equal(t, "notify", out.Steps.ContainerList[2].Name)
		assert.Equal(t, "slack", out.Steps.ContainerList[2].Image)
		assert.Equal(t, "frontend", out.Labels["com.example.team"])
		assert.Equal(t, "build", out.Labels["com.example.type"])
		assert.Equal(t, "lint", out.DependsOn[0])
		assert.Equal(t, "test", out.DependsOn[1])
		assert.EqualValues(t, []string{"success", "failure"}, out.When.Constraints[0].Status)
		assert.False(t, out.SkipClone)
	})

	t.Run("Should fail on invalid yaml", func(t *testing.T) {
		_, err := ParseString("notvalid")
		assert.Error(t, err)
	})

	t.Run("Should handle simple yaml anchors", func(t *testing.T) {
		out, err := ParseString(simpleYamlAnchors)
		assert.NoError(t, err)
		assert.Equal(t, "notify_success", out.Steps.ContainerList[0].Name)
		assert.Equal(t, "plugins/slack", out.Steps.ContainerList[0].Image)
	})

	t.Run("Should unmarshal variables", func(t *testing.T) {
		out, err := ParseString(sampleVarYaml)
		assert.NoError(t, err)
		assert.Equal(t, "notify_fail", out.Steps.ContainerList[0].Name)
		assert.Equal(t, "plugins/slack", out.Steps.ContainerList[0].Image)
		assert.Equal(t, "notify_success", out.Steps.ContainerList[1].Name)
		assert.Equal(t, "plugins/slack", out.Steps.ContainerList[1].Image)

		assert.Empty(t, out.Steps.ContainerList[0].When.Constraints)
		assert.Equal(t, "notify_success", out.Steps.ContainerList[1].Name)
		assert.Equal(t, "plugins/slack", out.Steps.ContainerList[1].Image)
		assert.Equal(t, yaml_base_types.StringOrSlice{"push"}, out.Steps.ContainerList[1].When.Constraints[0].Event)
	})
}

func TestMatch(t *testing.T) {
	matchConfig, err := ParseString(sampleYaml)
	assert.NoError(t, err)

	t.Run("Should match event tester", func(t *testing.T) {
		match, err := matchConfig.When.Match(metadata.Metadata{
			Curr: metadata.Pipeline{
				Event: "tester",
			},
		}, false, nil)
		assert.True(t, match)
		assert.NoError(t, err)
	})

	t.Run("Should match event tester2", func(t *testing.T) {
		match, err := matchConfig.When.Match(metadata.Metadata{
			Curr: metadata.Pipeline{
				Event: "tester2",
			},
		}, false, nil)
		assert.True(t, match)
		assert.NoError(t, err)
	})

	t.Run("Should match branch tester", func(t *testing.T) {
		match, err := matchConfig.When.Match(metadata.Metadata{
			Curr: metadata.Pipeline{
				Commit: metadata.Commit{
					Branch: "tester",
				},
			},
		}, true, nil)
		assert.True(t, match)
		assert.NoError(t, err)
	})

	t.Run("Should not match event push", func(t *testing.T) {
		match, err := matchConfig.When.Match(metadata.Metadata{
			Curr: metadata.Pipeline{
				Event: "push",
			},
		}, false, nil)
		assert.False(t, match)
		assert.NoError(t, err)
	})
}

func TestParseLegacy(t *testing.T) {
	sampleYamlPipeline := `
labels:
  platform: linux/amd64

steps:
  say hello:
    image: bash
    commands: echo hello
`

	sampleYamlPipelineLegacyIgnore := `
platform: windows/amd64
labels:
  platform: linux/amd64

steps:
  say hello:
    image: bash
    commands: echo hello

pipeline:
  old crap:
    image: bash
    commands: meh!
`

	workflow1, err := ParseString(sampleYamlPipeline)
	require.NoError(t, err)

	workflow2, err := ParseString(sampleYamlPipelineLegacyIgnore)
	require.NoError(t, err)

	assert.EqualValues(t, workflow1, workflow2)
	assert.Len(t, workflow1.Steps.ContainerList, 1)
	assert.EqualValues(t, "say hello", workflow1.Steps.ContainerList[0].Name)
}

var sampleYaml = `
image: hello-world
when:
  - event:
    - tester
    - tester2
    status: [ success, failure ]
  - branch:
    - tester
    status: [ success, failure ]
workspace:
  path: src/github.com/octocat/hello-world
  base: /go
steps:
  test:
    image: golang
    commands:
      - go install
      - go test
  build:
    image: golang
    commands:
      - go build
    when:
      event: push
    depends_on: []
  notify:
    image: slack
    settings:
      channel: dev
    when:
      event: failure
services:
  database:
    image: mysql
labels:
  com.example.type: "build"
  com.example.team: "frontend"
depends_on:
  - lint
  - test
`

var simpleYamlAnchors = `
vars:
  image: &image plugins/slack
steps:
  notify_success:
    image: *image
`

var sampleVarYaml = `
variables: &SLACK
  image: plugins/slack
steps:
  notify_fail: *SLACK
  notify_success:
    << : *SLACK
    when:
      event: push
  echo:
    when:
    - path: wow.sh
      repo: "test"
      branch:
        exclude: main
    - path:
      - test.yaml
      - test.zig
    - path:
        exclude: a
        on_empty: true
    - ref: ref/tags/v1
      path:
  env:
    image: print
    environment:
      DRIVER: next
      PLATFORM: linux
`

func TestReSerialize(t *testing.T) {
	work1, err := ParseString(sampleVarYaml)
	require.NoError(t, err)

	work1Bin, err := yaml.Marshal(work1)
	require.NoError(t, err)

	assert.EqualValues(t, `steps:
    - name: notify_fail
      image: plugins/slack
    - name: notify_success
      image: plugins/slack
      when:
        event: push
    - name: echo
      when:
        - repo: test
          branch:
            exclude: main
          path: wow.sh
        - path:
            - test.yaml
            - test.zig
        - path:
            exclude: a
        - ref: ref/tags/v1
    - name: env
      image: print
      environment:
        DRIVER: next
        PLATFORM: linux
`, string(work1Bin))

	work2, err := ParseString(sampleYaml)
	require.NoError(t, err)

	workBin2, err := yaml.Marshal(work2)
	require.NoError(t, err)

	// TODO: fix "steps.[1].depends_on: []" to be re-serialized!
	assert.EqualValues(t, `when:
    - status:
        - success
        - failure
      event:
        - tester
        - tester2
    - branch: tester
      status:
        - success
        - failure
workspace:
    base: /go
    path: src/github.com/octocat/hello-world
steps:
    - name: test
      image: golang
      commands:
        - go install
        - go test
    - name: build
      image: golang
      commands: go build
      when:
        event: push
    - name: notify
      image: slack
      settings:
        channel: dev
      when:
        event: failure
services:
    - name: database
      image: mysql
labels:
    com.example.team: frontend
    com.example.type: build
depends_on:
    - lint
    - test
`, string(workBin2))
}

func TestSlice(t *testing.T) {
	out, err := ParseString(sampleYaml)
	require.NoError(t, err)

	t.Run("should marshal a not set slice to nil", func(t *testing.T) {
		assert.Equal(t, "test", out.Steps.ContainerList[0].Name)
		assert.Nil(t, out.Steps.ContainerList[0].DependsOn)
		assert.Empty(t, out.Steps.ContainerList[0].DependsOn)
	})

	t.Run("should marshal an empty slice", func(t *testing.T) {
		assert.Equal(t, "build", out.Steps.ContainerList[1].Name)
		assert.NotNil(t, out.Steps.ContainerList[1].DependsOn)
		assert.Empty(t, (out.Steps.ContainerList[1].DependsOn))
	})
}
