// Copyright © 2016 Asteris, LLC
//
// 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 human_test

import (
	"errors"
	"testing"

	"github.com/asteris-llc/converge/graph"
	"github.com/asteris-llc/converge/graph/node"
	pp "github.com/asteris-llc/converge/prettyprinters"
	"github.com/asteris-llc/converge/prettyprinters/human"
	"github.com/asteris-llc/converge/resource"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

var (
	defaultPrinter     = human.New()
	printerOnlyChanged = human.NewFiltered(human.ShowOnlyChanged)
	printerHideByKind  = human.NewFiltered(human.HideByKind("param"))
)

func TestSatisfiesInterface(t *testing.T) {
	t.Parallel()

	assert.Implements(t, (*pp.BasePrinter)(nil), new(human.Printer))
	assert.Implements(t, (*pp.NodePrinter)(nil), new(human.Printer))
	assert.Implements(t, (*pp.GraphPrinter)(nil), new(human.Printer))
}

func testFinishPP(t *testing.T, in Printable, out string) {
	g := graph.New()
	g.Add(node.New("root/task", in))

	printer := human.New()
	printer.InitColors()
	str, err := printer.FinishPP(g)

	require.Nil(t, err)
	assert.Equal(t, out, str.String())
}

func testFinishPPMultiNode(t *testing.T, nodeList []string, in []Printable, out string) {
	g := graph.New()
	for i, val := range in {
		g.Add(node.New(nodeList[i], val))
	}

	printer := human.New()
	printer.InitColors()
	str, err := printer.FinishPP(g)

	require.Nil(t, err)
	assert.Equal(t, out, str.String())
}

func TestFinishPPSuccess(t *testing.T) {
	t.Parallel()

	testFinishPP(
		t,
		Printable{"a": "b"},
		"Summary: 0 errors, 1 changes\n",
	)
}

func TestFinishPPError(t *testing.T) {
	t.Parallel()

	t.Run("errors", func(t *testing.T) {
		testFinishPP(
			t,
			Printable{"error": "test"},
			"Errors:\n * root/task: test\n\nSummary: 1 errors, 0 changes\n",
		)
	})

	t.Run("dependency errors", func(t *testing.T) {
		t.Run("dep errors only", func(t *testing.T) {
			testFinishPP(
				t,
				Printable{"error": "error in dependency root/test"},
				"Failed due to failing dependency:\n * root/task: error in dependency root/test\n\nSummary: 0 errors, 0 changes, 1 dependency errors\n",
			)
		})

		t.Run("with errors", func(t *testing.T) {
			testFinishPPMultiNode(
				t,
				[]string{"root/test", "root/subtest"},
				[]Printable{{"error": "test"},
					{"error": "error in dependency root/test"}},
				"Errors:\n * root/test: test\n\nFailed due to failing dependency:\n * root/subtest: error in dependency root/test\n\nSummary: 1 errors, 0 changes, 1 dependency errors\n",
			)
		})

		t.Run("with changes and errors", func(t *testing.T) {
			testFinishPPMultiNode(
				t,
				[]string{"root/test", "root/subtest", "root/task"},
				[]Printable{{"error": "test"},
					{"error": "error in dependency root/test"},
					{"a": "b"}},
				"Errors:\n * root/test: test\n\nFailed due to failing dependency:\n * root/subtest: error in dependency root/test\n\nSummary: 1 errors, 1 changes, 1 dependency errors\n",
			)
		})
	})
}

func testDrawNodes(t *testing.T, in Printable, out string) {
	printer := human.New()
	printer.InitColors()
	testDrawNodesCustomPrinter(
		t,
		printer,
		"root",
		in,
		out,
	)
}

func testDrawNodesCustomPrinter(t *testing.T, h *human.Printer, id string, in Printable, out string) {
	g := graph.New()
	g.Add(node.New(id, in))

	str, err := h.DrawNode(g, id)

	require.Nil(t, err)
	assert.Equal(t, out, str.String())
}

func benchmarkDrawNodes(in Printable) {
	benchmarkDrawNodesCustomPrinter(
		defaultPrinter,
		"root",
		in,
	)
}

func benchmarkDrawNodesCustomPrinter(h *human.Printer, id string, in Printable) {
	g := graph.New()
	g.Add(node.New(id, in))

	h.DrawNode(g, id)
}

func TestDrawNodeNoChanges(t *testing.T) {
	t.Parallel()

	testDrawNodes(
		t,
		Printable{},
		"root:\n Messages:\n Has Changes: no\n Changes: No changes\n\n",
	)
}

func BenchmarkDrawNodeNoChanges(b *testing.B) {
	for i := 0; i < b.N; i++ {
		benchmarkDrawNodes(
			Printable{},
		)
	}
}

func TestDrawNodeNoChangesFiltered(t *testing.T) {
	t.Parallel()
	printerOnlyChanged.InitColors()

	testDrawNodesCustomPrinter(
		t,
		printerOnlyChanged,
		"root",
		Printable{},
		"",
	)
}

func BenchmarkDrawNodeNoChangesFiltered(b *testing.B) {
	for i := 0; i < b.N; i++ {
		benchmarkDrawNodesCustomPrinter(
			printerOnlyChanged,
			"root",
			Printable{},
		)
	}
}

func TestDrawNodeMetaFiltered(t *testing.T) {
	t.Parallel()
	printerHideByKind.InitColors()

	testDrawNodesCustomPrinter(
		t,
		printerHideByKind,
		"param.test",
		Printable{},
		"",
	)
}

func BenchmarkDrawNodeMetaFiltered(b *testing.B) {
	for i := 0; i < b.N; i++ {
		benchmarkDrawNodesCustomPrinter(
			printerHideByKind,
			"param.test",
			Printable{},
		)
	}
}

func TestDrawNodeChanges(t *testing.T) {
	t.Parallel()

	testDrawNodes(
		t,
		Printable{"a": "b"},
		"root:\n Messages:\n Has Changes: yes\n Changes:\n  a: \"\" => \"b\"\n\n",
	)
}

func BenchmarkDrawNodeChanges(b *testing.B) {
	for i := 0; i < b.N; i++ {
		benchmarkDrawNodes(
			Printable{"a": "b"},
		)
	}
}

func TestDrawNodeError(t *testing.T) {
	t.Parallel()

	testDrawNodes(
		t,
		Printable{"error": "x"},
		"root:\n Error: x\n Messages:\n Has Changes: yes\n Changes:\n  error: \"\" => \"x\"\n\n",
	)
}

// TestDrawNodeWarning tests to ensure that warnings work correctly
func TestDrawNodeWarning(t *testing.T) {
	t.Parallel()

	testDrawNodes(
		t,
		Printable{"a": "b", "warning": "x"},
		"root:\n Warning: x\n Messages:\n Has Changes: yes\n Changes:\n  a: \"\" => \"b\"\n\n",
	)
}

func BenchmarkDrawNodeError(b *testing.B) {
	for i := 0; i < b.N; i++ {
		benchmarkDrawNodes(
			Printable{"error": "x"},
		)
	}
}

// printable stub

type Printable map[string]string

func (p Printable) Messages() []string {
	return []string{}
}

func (p Printable) Changes() map[string]resource.Diff {
	out := map[string]resource.Diff{}

	for key, value := range p {
		if key == "warning" { // ignore special warning key
			continue
		}
		out[key] = resource.TextDiff{Values: [2]string{"", value}}
	}

	return out
}

func (p Printable) HasChanges() bool {
	return len(p) > 0
}

func (p Printable) Error() error {
	err, ok := p["error"]
	if !ok {
		return nil
	}

	return errors.New(err)
}

// Warning generates a warning
func (p Printable) Warning() string {
	return p["warning"]
}
