package types

import (
	"os"
	"strconv"
	"strings"
	"sync"
	"unicode"

	"github.com/onsi/ginkgo/v2/formatter"
)

type Deprecation struct {
	Message string
	DocLink string
	Version string
}

type deprecations struct{}

var Deprecations = deprecations{}

func (d deprecations) CustomReporter() Deprecation {
	return Deprecation{
		Message: "Support for custom reporters has been removed in V2.  Please read the documentation linked to below for Ginkgo's new behavior and for a migration path:",
		DocLink: "removed-custom-reporters",
		Version: "1.16.0",
	}
}

func (d deprecations) Async() Deprecation {
	return Deprecation{
		Message: "You are passing a Done channel to a test node to test asynchronous behavior.  This is deprecated in Ginkgo V2.  Your test will run synchronously and the timeout will be ignored.",
		DocLink: "removed-async-testing",
		Version: "1.16.0",
	}
}

func (d deprecations) Measure() Deprecation {
	return Deprecation{
		Message: "Measure is deprecated and will be removed in Ginkgo V2.  Please migrate to gomega/gmeasure.",
		DocLink: "removed-measure",
		Version: "1.16.3",
	}
}

func (d deprecations) ParallelNode() Deprecation {
	return Deprecation{
		Message: "GinkgoParallelNode is deprecated and will be removed in Ginkgo V2.  Please use GinkgoParallelProcess instead.",
		DocLink: "renamed-ginkgoparallelnode",
		Version: "1.16.4",
	}
}

func (d deprecations) CurrentGinkgoTestDescription() Deprecation {
	return Deprecation{
		Message: "CurrentGinkgoTestDescription() is deprecated in Ginkgo V2.  Use CurrentSpecReport() instead.",
		DocLink: "changed-currentginkgotestdescription",
		Version: "1.16.0",
	}
}

func (d deprecations) Convert() Deprecation {
	return Deprecation{
		Message: "The convert command is deprecated in Ginkgo V2",
		DocLink: "removed-ginkgo-convert",
		Version: "1.16.0",
	}
}

func (d deprecations) Blur() Deprecation {
	return Deprecation{
		Message: "The blur command is deprecated in Ginkgo V2.  Use 'ginkgo unfocus' instead.",
		Version: "1.16.0",
	}
}

func (d deprecations) Nodot() Deprecation {
	return Deprecation{
		Message: "The nodot command is deprecated in Ginkgo V2.  Please either dot-import Ginkgo or use the package identifier in your code to references objects and types provided by Ginkgo and Gomega.",
		DocLink: "removed-ginkgo-nodot",
		Version: "1.16.0",
	}
}

type DeprecationTracker struct {
	deprecations map[Deprecation][]CodeLocation
	lock         *sync.Mutex
}

func NewDeprecationTracker() *DeprecationTracker {
	return &DeprecationTracker{
		deprecations: map[Deprecation][]CodeLocation{},
		lock:         &sync.Mutex{},
	}
}

func (d *DeprecationTracker) TrackDeprecation(deprecation Deprecation, cl ...CodeLocation) {
	ackVersion := os.Getenv("ACK_GINKGO_DEPRECATIONS")
	if deprecation.Version != "" && ackVersion != "" {
		ack := ParseSemVer(ackVersion)
		version := ParseSemVer(deprecation.Version)
		if ack.GreaterThanOrEqualTo(version) {
			return
		}
	}

	d.lock.Lock()
	defer d.lock.Unlock()
	if len(cl) == 1 {
		d.deprecations[deprecation] = append(d.deprecations[deprecation], cl[0])
	} else {
		d.deprecations[deprecation] = []CodeLocation{}
	}
}

func (d *DeprecationTracker) DidTrackDeprecations() bool {
	d.lock.Lock()
	defer d.lock.Unlock()
	return len(d.deprecations) > 0
}

func (d *DeprecationTracker) DeprecationsReport() string {
	d.lock.Lock()
	defer d.lock.Unlock()
	out := formatter.F("{{light-yellow}}You're using deprecated Ginkgo functionality:{{/}}\n")
	out += formatter.F("{{light-yellow}}============================================={{/}}\n")
	for deprecation, locations := range d.deprecations {
		out += formatter.Fi(1, "{{yellow}}"+deprecation.Message+"{{/}}\n")
		if deprecation.DocLink != "" {
			out += formatter.Fi(1, "{{bold}}Learn more at:{{/}} {{cyan}}{{underline}}https://onsi.github.io/ginkgo/MIGRATING_TO_V2#%s{{/}}\n", deprecation.DocLink)
		}
		for _, location := range locations {
			out += formatter.Fi(2, "{{gray}}%s{{/}}\n", location)
		}
	}
	out += formatter.F("\n{{gray}}To silence deprecations that can be silenced set the following environment variable:{{/}}\n")
	out += formatter.Fi(1, "{{gray}}ACK_GINKGO_DEPRECATIONS=%s{{/}}\n", VERSION)
	return out
}

type SemVer struct {
	Major int
	Minor int
	Patch int
}

func (s SemVer) GreaterThanOrEqualTo(o SemVer) bool {
	return (s.Major > o.Major) ||
		(s.Major == o.Major && s.Minor > o.Minor) ||
		(s.Major == o.Major && s.Minor == o.Minor && s.Patch >= o.Patch)
}

func ParseSemVer(semver string) SemVer {
	out := SemVer{}
	semver = strings.TrimFunc(semver, func(r rune) bool {
		return !(unicode.IsNumber(r) || r == '.')
	})
	components := strings.Split(semver, ".")
	if len(components) > 0 {
		out.Major, _ = strconv.Atoi(components[0])
	}
	if len(components) > 1 {
		out.Minor, _ = strconv.Atoi(components[1])
	}
	if len(components) > 2 {
		out.Patch, _ = strconv.Atoi(components[2])
	}
	return out
}
