package resolver

import (
	"context"

	"github.com/0xERR0R/blocky/config"
	. "github.com/0xERR0R/blocky/helpertest"
	"github.com/0xERR0R/blocky/log"
	. "github.com/0xERR0R/blocky/model"
	"github.com/miekg/dns"
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
)

var _ = Describe("UpstreamTreeResolver", Label("upstreamTreeResolver"), func() {
	var (
		sut       Resolver
		sutConfig config.Upstreams

		err error

		ctx      context.Context
		cancelFn context.CancelFunc
	)

	BeforeEach(func() {
		ctx, cancelFn = context.WithCancel(context.Background())
		DeferCleanup(cancelFn)

		sutConfig = defaultUpstreamsConfig
	})

	JustBeforeEach(func() {
		sut, err = NewUpstreamTreeResolver(ctx, sutConfig, systemResolverBootstrap)
	})

	When("it has no configuration", func() {
		BeforeEach(func() {
			sutConfig = config.Upstreams{}
		})

		It("should return error", func() {
			Expect(err).To(HaveOccurred())
			Expect(err).To(MatchError(ContainSubstring("no external DNS resolvers configured")))
			Expect(sut).To(BeNil())
		})
	})

	When("it has only default group", func() {
		BeforeEach(func() {
			sutConfig.Groups = config.UpstreamGroups{
				upstreamDefaultCfgName: {
					{Host: "wrong"},
					{Host: "127.0.0.1"},
				},
			}
		})

		When("strategy is parallel", func() {
			BeforeEach(func() {
				sutConfig.Strategy = config.UpstreamStrategyParallelBest
			})

			It("returns the resolver directly", func() {
				Expect(err).ToNot(HaveOccurred())

				_, ok := sut.(*ParallelBestResolver)
				Expect(ok).Should(BeTrue())
			})
		})

		When("strategy is strict", func() {
			BeforeEach(func() {
				sutConfig.Strategy = config.UpstreamStrategyStrict
			})

			It("returns the resolver directly", func() {
				Expect(err).ToNot(HaveOccurred())

				_, ok := sut.(*StrictResolver)
				Expect(ok).Should(BeTrue())
			})
		})
	})

	When("it has multiple groups", func() {
		BeforeEach(func() {
			sutConfig.Groups = config.UpstreamGroups{
				upstreamDefaultCfgName: {
					{Host: "wrong"},
					{Host: "127.0.0.1"},
				},
				"test": {
					{Host: "some-resolver"},
				},
			}
		})

		Describe("Type", func() {
			It("does not return error", func() {
				Expect(err).ToNot(HaveOccurred())
			})
			It("follows conventions", func() {
				expectValidResolverType(sut)
			})
			It("returns upstream_tree", func() {
				Expect(sut.Type()).To(Equal(upstreamTreeResolverType))
			})
		})

		Describe("Configuration output", func() {
			It("should return configuration", func() {
				Expect(sut.IsEnabled()).Should(BeTrue())

				logger, hook := log.NewMockEntry()
				sut.LogConfig(logger)
				Expect(hook.Calls).ToNot(BeEmpty())
			})
		})

		Describe("Name", func() {
			var utrSut *UpstreamTreeResolver
			JustBeforeEach(func() {
				utrSut = sut.(*UpstreamTreeResolver)
			})

			It("should contain correct resolver", func() {
				name := utrSut.Name()
				Expect(name).ShouldNot(BeEmpty())
				Expect(name).Should(ContainSubstring(upstreamTreeResolverType))
			})
		})

		When("init strategy is failOnError", func() {
			BeforeEach(func() {
				sutConfig.Init.Strategy = config.InitStrategyFailOnError
			})

			It("should fail", func() {
				Expect(err).To(HaveOccurred())
				Expect(err).To(MatchError(ContainSubstring("no valid upstream")))
				Expect(sut).To(BeNil())
			})
		})

		When("client specific resolvers are defined", func() {
			groups := map[string]string{
				upstreamDefaultCfgName: "127.0.0.1",
				"laptop":               "127.0.0.2",
				"client-*-m":           "127.0.0.3",
				"client[0-9]":          "127.0.0.4",
				"192.168.178.33":       "127.0.0.5",
				"10.43.8.67/28":        "127.0.0.6",
				"name-matches1":        "127.0.0.7",
				"name-matches*":        "127.0.0.8",
			}

			BeforeEach(func() {
				sutConfig.Groups = make(config.UpstreamGroups, len(groups))

				for group, ip := range groups {
					Expect(ip).ShouldNot(BeNil())

					server := NewMockUDPUpstreamServer().WithAnswerRR("example.com 123 IN A " + ip)
					sutConfig.Groups[group] = []config.Upstream{server.Start()}
				}
			})

			It("Should use default if client name or IP don't match", func() {
				request := newRequestWithClient("example.com.", A, "192.168.178.55", "test")

				Expect(sut.Resolve(ctx, request)).
					Should(
						SatisfyAll(
							BeDNSRecord("example.com.", A, groups["default"]),
							HaveResponseType(ResponseTypeRESOLVED),
							HaveReturnCode(dns.RcodeSuccess),
						))
			})
			It("Should use client specific resolver if client name matches exact", func() {
				request := newRequestWithClient("example.com.", A, "192.168.178.55", "laptop")

				Expect(sut.Resolve(ctx, request)).
					Should(
						SatisfyAll(
							BeDNSRecord("example.com.", A, groups["laptop"]),
							HaveResponseType(ResponseTypeRESOLVED),
							HaveReturnCode(dns.RcodeSuccess),
						))
			})
			It("Should use client specific resolver if client name matches with wildcard", func() {
				request := newRequestWithClient("example.com.", A, "192.168.178.55", "client-test-m")

				Expect(sut.Resolve(ctx, request)).
					Should(
						SatisfyAll(
							BeDNSRecord("example.com.", A, groups["client-*-m"]),
							HaveResponseType(ResponseTypeRESOLVED),
							HaveReturnCode(dns.RcodeSuccess),
						))
			})
			It("Should use client specific resolver if client name matches with range wildcard", func() {
				request := newRequestWithClient("example.com.", A, "192.168.178.55", "client7")

				Expect(sut.Resolve(ctx, request)).
					Should(
						SatisfyAll(
							BeDNSRecord("example.com.", A, groups["client[0-9]"]),
							HaveResponseType(ResponseTypeRESOLVED),
							HaveReturnCode(dns.RcodeSuccess),
						))
			})
			It("Should use client specific resolver if client IP matches", func() {
				request := newRequestWithClient("example.com.", A, "192.168.178.33", "noname")

				Expect(sut.Resolve(ctx, request)).
					Should(
						SatisfyAll(
							BeDNSRecord("example.com.", A, groups["192.168.178.33"]),
							HaveResponseType(ResponseTypeRESOLVED),
							HaveReturnCode(dns.RcodeSuccess),
						))
			})
			It("Should use client specific resolver if client name (containing IP) matches", func() {
				request := newRequestWithClient("example.com.", A, "0.0.0.0", "192.168.178.33")

				Expect(sut.Resolve(ctx, request)).
					Should(
						SatisfyAll(
							BeDNSRecord("example.com.", A, groups["192.168.178.33"]),
							HaveResponseType(ResponseTypeRESOLVED),
							HaveReturnCode(dns.RcodeSuccess),
						))
			})
			It("Should use client specific resolver if client's CIDR (10.43.8.64 - 10.43.8.79) matches", func() {
				request := newRequestWithClient("example.com.", A, "10.43.8.70", "noname")

				Expect(sut.Resolve(ctx, request)).
					Should(
						SatisfyAll(
							BeDNSRecord("example.com.", A, groups["10.43.8.67/28"]),
							HaveResponseType(ResponseTypeRESOLVED),
							HaveReturnCode(dns.RcodeSuccess),
						))
			})
			It("Should use exact IP match before client name match", func() {
				request := newRequestWithClient("example.com.", A, "192.168.178.33", "laptop")

				Expect(sut.Resolve(ctx, request)).
					Should(
						SatisfyAll(
							BeDNSRecord("example.com.", A, groups["192.168.178.33"]),
							HaveResponseType(ResponseTypeRESOLVED),
							HaveReturnCode(dns.RcodeSuccess),
						))
			})
			It("Should use client name match before CIDR match", func() {
				request := newRequestWithClient("example.com.", A, "10.43.8.70", "laptop")

				Expect(sut.Resolve(ctx, request)).
					Should(
						SatisfyAll(
							BeDNSRecord("example.com.", A, groups["laptop"]),
							HaveResponseType(ResponseTypeRESOLVED),
							HaveReturnCode(dns.RcodeSuccess),
						))
			})
			It("Should use one of the matching resolvers & log warning", func() {
				logger, hook := log.NewMockEntry()

				ctx, _ = log.NewCtx(ctx, logger)

				Expect(sut.Resolve(ctx, newRequestWithClient("example.com.", A, "0.0.0.0", "name-matches1"))).
					Should(
						SatisfyAll(
							SatisfyAny(
								BeDNSRecord("example.com.", A, groups["name-matches1"]),
								BeDNSRecord("example.com.", A, groups["name-matches*"]),
							),
							HaveResponseType(ResponseTypeRESOLVED),
							HaveReturnCode(dns.RcodeSuccess),
						))

				Expect(hook.Messages).Should(ContainElement(ContainSubstring("client matches multiple groups")))
			})
		})
	})
})
