package runtime

import (
	"fmt"
	"os"
	"testing"

	"github.com/lunixbochs/vtclean"
	"github.com/spf13/afero"
	"github.com/spf13/viper"

	"github.com/wagoodman/dive/dive"
	"github.com/wagoodman/dive/dive/image"
	"github.com/wagoodman/dive/dive/image/docker"
)

type defaultResolver struct{}

func (r *defaultResolver) Name() string {
	return "default-resolver"
}

func (r *defaultResolver) Extract(id string, l string, p string) error {
	return nil
}

func (r *defaultResolver) Fetch(id string) (*image.Image, error) {
	archive, err := docker.TestLoadArchive("../.data/test-docker-image.tar")
	if err != nil {
		return nil, err
	}
	return archive.ToImage()
}

func (r *defaultResolver) Build(args []string) (*image.Image, error) {
	return r.Fetch("")
}

type failedBuildResolver struct{}

func (r *failedBuildResolver) Name() string {
	return "failed-build-resolver"
}

func (r *failedBuildResolver) Extract(id string, l string, p string) error {
	return fmt.Errorf("some extract failure")
}

func (r *failedBuildResolver) Fetch(id string) (*image.Image, error) {
	archive, err := docker.TestLoadArchive("../.data/test-docker-image.tar")
	if err != nil {
		return nil, err
	}
	return archive.ToImage()
}

func (r *failedBuildResolver) Build(args []string) (*image.Image, error) {
	return nil, fmt.Errorf("some build failure")
}

type failedFetchResolver struct{}

func (r *failedFetchResolver) Name() string {
	return "failed-fetch-resolver"
}

func (r *failedFetchResolver) Extract(id string, l string, p string) error {
	return fmt.Errorf("some extract failure")
}

func (r *failedFetchResolver) Fetch(id string) (*image.Image, error) {
	return nil, fmt.Errorf("some fetch failure")
}

func (r *failedFetchResolver) Build(args []string) (*image.Image, error) {
	return nil, fmt.Errorf("some build failure")
}

// func showEvents(events []testEvent) {
// 	for _, e := range events {
// 		fmt.Printf("{stdout:\"%s\", stderr:\"%s\", errorOnExit: %v, errMessage: \"%s\"},\n",
// 			strings.Replace(vtclean.Clean(e.stdout, false), "\n", "\\n", -1),
// 			strings.Replace(vtclean.Clean(e.stderr, false), "\n", "\\n", -1),
// 			e.errorOnExit,
// 			e.errMessage)
// 	}
// }

type testEvent struct {
	stdout      string
	stderr      string
	errMessage  string
	errorOnExit bool
}

func newTestEvent(e event) testEvent {
	var errMsg string
	if e.err != nil {
		errMsg = e.err.Error()
	}
	return testEvent{
		stdout:      e.stdout,
		stderr:      e.stderr,
		errMessage:  errMsg,
		errorOnExit: e.errorOnExit,
	}
}

func configureCi() *viper.Viper {
	ciConfig := viper.New()
	ciConfig.SetDefault("rules.lowestEfficiency", "0.9")
	ciConfig.SetDefault("rules.highestWastedBytes", "1000")
	ciConfig.SetDefault("rules.highestUserWastedPercent", "0.1")
	return ciConfig
}

func TestRun(t *testing.T) {
	table := map[string]struct {
		resolver image.Resolver
		options  Options
		events   []testEvent
	}{
		"fetch-case": {
			resolver: &defaultResolver{},
			options: Options{
				Ci:         false,
				Image:      "dive-example",
				Source:     dive.SourceDockerEngine,
				ExportFile: "",
				CiConfig:   nil,
				BuildArgs:  nil,
			},
			events: []testEvent{
				{stdout: "Image Source: docker://dive-example", stderr: "", errorOnExit: false, errMessage: ""},
				{stdout: "Extracting image from default-resolver... (this can take a while for large images)", stderr: "", errorOnExit: false, errMessage: ""},
				{stdout: "Analyzing image...", stderr: "", errorOnExit: false, errMessage: ""},
				{stdout: "Building cache...", stderr: "", errorOnExit: false, errMessage: ""},
			},
		},
		"fetch-with-no-build-options-case": {
			resolver: &defaultResolver{},
			options: Options{
				Ci:         false,
				Image:      "dive-example",
				Source:     dive.SourceDockerEngine,
				ExportFile: "",
				CiConfig:   nil,
				// note: empty slice is passed
				BuildArgs: []string{},
			},
			events: []testEvent{
				{stdout: "Image Source: docker://dive-example", stderr: "", errorOnExit: false, errMessage: ""},
				{stdout: "Extracting image from default-resolver... (this can take a while for large images)", stderr: "", errorOnExit: false, errMessage: ""},
				{stdout: "Analyzing image...", stderr: "", errorOnExit: false, errMessage: ""},
				{stdout: "Building cache...", stderr: "", errorOnExit: false, errMessage: ""},
			},
		},
		"build-case": {
			resolver: &defaultResolver{},
			options: Options{
				Ci:         false,
				Image:      "dive-example",
				Source:     dive.SourceDockerEngine,
				ExportFile: "",
				CiConfig:   nil,
				BuildArgs:  []string{"an-option"},
			},
			events: []testEvent{
				{stdout: "Building image...", stderr: "", errorOnExit: false, errMessage: ""},
				{stdout: "Analyzing image...", stderr: "", errorOnExit: false, errMessage: ""},
				{stdout: "Building cache...", stderr: "", errorOnExit: false, errMessage: ""},
			},
		},
		"failed-fetch": {
			resolver: &failedFetchResolver{},
			options: Options{
				Ci:         false,
				Image:      "dive-example",
				Source:     dive.SourceDockerEngine,
				ExportFile: "",
				CiConfig:   nil,
				BuildArgs:  nil,
			},
			events: []testEvent{
				{stdout: "Image Source: docker://dive-example", stderr: "", errorOnExit: false, errMessage: ""},
				{stdout: "Extracting image from failed-fetch-resolver... (this can take a while for large images)", stderr: "", errorOnExit: false, errMessage: ""},
				{stdout: "", stderr: "cannot fetch image", errorOnExit: true, errMessage: "some fetch failure"},
			},
		},
		"failed-build": {
			resolver: &failedBuildResolver{},
			options: Options{
				Ci:         false,
				Image:      "doesn't-matter",
				Source:     dive.SourceDockerEngine,
				ExportFile: "",
				CiConfig:   nil,
				BuildArgs:  []string{"an-option"},
			},
			events: []testEvent{
				{stdout: "Building image...", stderr: "", errorOnExit: false, errMessage: ""},
				{stdout: "", stderr: "cannot build image", errorOnExit: true, errMessage: "some build failure"},
			},
		},
		"ci-go-case": {
			resolver: &defaultResolver{},
			options: Options{
				Ci:         true,
				Image:      "doesn't-matter",
				Source:     dive.SourceDockerEngine,
				ExportFile: "",
				CiConfig:   configureCi(),
				BuildArgs:  []string{"an-option"},
			},
			events: []testEvent{
				{stdout: "Building image...", stderr: "", errorOnExit: false, errMessage: ""},
				{stdout: "Analyzing image...", stderr: "", errorOnExit: false, errMessage: ""},
				{stdout: "  efficiency: 98.4421 %", stderr: "", errorOnExit: false, errMessage: ""},
				{stdout: "  wastedBytes: 32025 bytes (32 kB)", stderr: "", errorOnExit: false, errMessage: ""},
				{stdout: "  userWastedPercent: 48.3491 %", stderr: "", errorOnExit: false, errMessage: ""},
				{stdout: "Inefficient Files:\nCount  Wasted Space  File Path\n    2         13 kB  /root/saved.txt\n    2         13 kB  /root/example/somefile1.txt\n    2        6.4 kB  /root/example/somefile3.txt\nResults:\n  FAIL: highestUserWastedPercent: too many bytes wasted, relative to the user bytes added (%-user-wasted-bytes=0.4834911001404049 > threshold=0.1)\n  FAIL: highestWastedBytes: too many bytes wasted (wasted-bytes=32025 > threshold=1000)\n  PASS: lowestEfficiency\nResult:FAIL [Total:3] [Passed:1] [Failed:2] [Warn:0] [Skipped:0]\n", stderr: "", errorOnExit: false, errMessage: ""},
				{stdout: "", stderr: "", errorOnExit: true, errMessage: ""},
			},
		},
		"empty-ci-config-case": {
			resolver: &defaultResolver{},
			options: Options{
				Ci:         true,
				Image:      "doesn't-matter",
				Source:     dive.SourceDockerEngine,
				ExportFile: "",
				CiConfig:   viper.New(),
				BuildArgs:  []string{"an-option"},
			},
			events: []testEvent{
				{stdout: "Building image...", stderr: "", errorOnExit: false, errMessage: ""},
				{stdout: "Analyzing image...", stderr: "", errorOnExit: false, errMessage: ""},
				{stdout: "  efficiency: 98.4421 %", stderr: "", errorOnExit: false, errMessage: ""},
				{stdout: "  wastedBytes: 32025 bytes (32 kB)", stderr: "", errorOnExit: false, errMessage: ""},
				{stdout: "  userWastedPercent: 48.3491 %", stderr: "", errorOnExit: false, errMessage: ""},
				{stdout: "Inefficient Files:\nCount  Wasted Space  File Path\nNone\nResults:\n  MISCONFIGURED: highestUserWastedPercent: invalid config value (''): strconv.ParseFloat: parsing \"\": invalid syntax\n  MISCONFIGURED: highestWastedBytes: invalid config value (''): strconv.ParseFloat: parsing \"\": invalid syntax\n  MISCONFIGURED: lowestEfficiency: invalid config value (''): strconv.ParseFloat: parsing \"\": invalid syntax\nCI Misconfigured\n", stderr: "", errorOnExit: false, errMessage: ""},
				{stdout: "", stderr: "", errorOnExit: true, errMessage: ""},
			},
		},
		"export-go-case": {
			resolver: &defaultResolver{},
			options: Options{
				Ci:         true,
				Image:      "doesn't-matter",
				Source:     dive.SourceDockerEngine,
				ExportFile: "some-file.json",
				CiConfig:   configureCi(),
				BuildArgs:  []string{"an-option"},
			},
			events: []testEvent{
				{stdout: "Building image...", stderr: "", errorOnExit: false, errMessage: ""},
				{stdout: "Analyzing image...", stderr: "", errorOnExit: false, errMessage: ""},
				{stdout: "Exporting image to 'some-file.json'...", stderr: "", errorOnExit: false, errMessage: ""},
			},
		},
	}

	for name, test := range table {
		var ec = make(eventChannel)
		var events = make([]testEvent, 0)
		var filesystem = afero.NewMemMapFs()

		go run(false, test.options, test.resolver, ec, filesystem)

		for event := range ec {
			events = append(events, newTestEvent(event))
		}

		// fmt.Println(name)
		// showEvents(events)
		// fmt.Println()

		if len(test.events) != len(events) {
			t.Fatalf("%s.%s: expected # events='%v', got '%v'", t.Name(), name, len(test.events), len(events))
		}

		for idx, actualEvent := range events {
			expectedEvent := test.events[idx]

			if expectedEvent.errorOnExit != actualEvent.errorOnExit {
				t.Errorf("%s.%s: expected errorOnExit='%v', got '%v'", t.Name(), name, expectedEvent.errorOnExit, actualEvent.errorOnExit)
			}

			actualEventStdoutClean := vtclean.Clean(actualEvent.stdout, false)
			expectedEventStdoutClean := vtclean.Clean(expectedEvent.stdout, false)

			if expectedEventStdoutClean != actualEventStdoutClean {
				t.Errorf("%s.%s: expected stdout='%v', got '%v'", t.Name(), name, expectedEventStdoutClean, actualEventStdoutClean)
			}

			actualEventStderrClean := vtclean.Clean(actualEvent.stderr, false)
			expectedEventStderrClean := vtclean.Clean(expectedEvent.stderr, false)

			if expectedEventStderrClean != actualEventStderrClean {
				t.Errorf("%s.%s: expected stderr='%v', got '%v'", t.Name(), name, expectedEventStderrClean, actualEventStderrClean)
			}

			if expectedEvent.errMessage != actualEvent.errMessage {
				t.Errorf("%s.%s: expected error='%v', got '%v'", t.Name(), name, expectedEvent.errMessage, actualEvent.errMessage)
			}

			if test.options.ExportFile != "" {
				if _, err := filesystem.Stat(test.options.ExportFile); os.IsNotExist(err) {
					t.Errorf("%s.%s: expected export file but did not find one", t.Name(), name)
				}
			}
		}
	}
}
