package resolver

import (
	"context"
	"time"

	"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"
	"github.com/stretchr/testify/mock"
)

var _ = Describe("HostsFileResolver", func() {
	var (
		TTL = uint32(time.Now().Second())

		sut       *HostsFileResolver
		sutConfig config.HostsFile
		m         *mockResolver
		tmpDir    *TmpFolder
		tmpFile   *TmpFile
		err       error
		resp      *Response

		ctx      context.Context
		cancelFn context.CancelFunc
	)

	Describe("Type", func() {
		It("follows conventions", func() {
			expectValidResolverType(sut)
		})
	})

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

		tmpDir = NewTmpFolder("HostsFileResolver")
		tmpFile = writeHostFile(tmpDir)

		sutConfig = config.HostsFile{
			Sources:        config.NewBytesSources(tmpFile.Path),
			HostsTTL:       config.Duration(time.Duration(TTL) * time.Second),
			FilterLoopback: true,
			Loading: config.SourceLoading{
				RefreshPeriod:      -1,
				MaxErrorsPerSource: 5,
			},
		}
	})

	JustBeforeEach(func() {
		sut, err = NewHostsFileResolver(ctx, sutConfig, systemResolverBootstrap)
		Expect(err).Should(Succeed())

		m = &mockResolver{}
		m.On("Resolve", mock.Anything).Return(&Response{Res: new(dns.Msg)}, nil)
		sut.Next(m)
	})

	Describe("IsEnabled", func() {
		It("is true", func() {
			Expect(sut.IsEnabled()).Should(BeTrue())
		})
	})

	Describe("LogConfig", func() {
		It("should log something", func() {
			logger, hook := log.NewMockEntry()

			sut.LogConfig(logger)

			Expect(hook.Calls).ShouldNot(BeEmpty())
		})
	})

	Describe("Using hosts file", func() {
		When("Hosts file cannot be located", func() {
			BeforeEach(func() {
				sutConfig = config.HostsFile{
					Sources:  config.NewBytesSources("/this/file/does/not/exist"),
					HostsTTL: config.Duration(time.Duration(TTL) * time.Second),
				}
			})
			It("should not parse any hosts", func() {
				Expect(sut.cfg.Sources).ShouldNot(BeEmpty())
				Expect(sut.hosts.v4.hosts).Should(BeEmpty())
				Expect(sut.hosts.v6.hosts).Should(BeEmpty())
				Expect(sut.hosts.v4.aliases).Should(BeEmpty())
				Expect(sut.hosts.v6.aliases).Should(BeEmpty())
				Expect(sut.hosts.isEmpty()).Should(BeTrue())
			})
			It("should go to next resolver on query", func() {
				Expect(sut.Resolve(ctx, newRequest("example.com.", A))).
					Should(
						SatisfyAll(
							HaveResponseType(ResponseTypeRESOLVED),
							HaveReturnCode(dns.RcodeSuccess),
						))
				m.AssertExpectations(GinkgoT())
			})
		})

		When("Hosts file is not set", func() {
			BeforeEach(func() {
				sutConfig.Deprecated.Filepath = new(config.BytesSource)
				sutConfig.Sources = make([]config.BytesSource, 0)
			})
			JustBeforeEach(func() {
				err = sut.loadSources(ctx)
				Expect(err).Should(Succeed())
			})
			It("should go to next resolver on query", func() {
				Expect(sut.Resolve(ctx, newRequest("example.com.", A))).
					Should(
						SatisfyAll(
							HaveResponseType(ResponseTypeRESOLVED),
							HaveReturnCode(dns.RcodeSuccess),
						))
				m.AssertExpectations(GinkgoT())
			})
		})

		When("Hosts file can be located", func() {
			It("should parse it successfully", func() {
				Expect(sut).ShouldNot(BeNil())
				Expect(sut.hosts.v4.hosts).Should(HaveLen(5))
				Expect(sut.hosts.v6.hosts).Should(HaveLen(2))
				Expect(sut.hosts.v4.aliases).Should(HaveLen(4))
				Expect(sut.hosts.v6.aliases).Should(HaveLen(2))
			})

			When("filterLoopback is false", func() {
				BeforeEach(func() {
					sutConfig.FilterLoopback = false
				})

				It("should parse it successfully", func() {
					Expect(sut).ShouldNot(BeNil())
					Expect(sut.hosts.v4.hosts).Should(HaveLen(7))
					Expect(sut.hosts.v6.hosts).Should(HaveLen(3))
					Expect(sut.hosts.v4.aliases).Should(HaveLen(5))
					Expect(sut.hosts.v6.aliases).Should(HaveLen(2))
				})
			})
		})

		When("Hosts file has too many errors", func() {
			BeforeEach(func() {
				tmpFile = tmpDir.CreateStringFile("hosts-too-many-errors.txt",
					"invalidip localhost",
					"127.0.0.1 localhost", // ok
					"127.0.0.1 # no host",
					"127.0.0.1 invalidhost!",
					"a.b.c.d localhost",
					"127.0.0.x localhost",
					"256.0.0.1 localhost",
				)

				sutConfig.Sources = config.NewBytesSources(tmpFile.Path)
			})

			It("should not be used", func() {
				Expect(sut).ShouldNot(BeNil())
				Expect(sut.cfg.Sources).ShouldNot(BeEmpty())
				Expect(sut.hosts.v4.hosts).Should(BeEmpty())
				Expect(sut.hosts.v6.hosts).Should(BeEmpty())
				Expect(sut.hosts.v4.aliases).Should(BeEmpty())
				Expect(sut.hosts.v6.aliases).Should(BeEmpty())
			})
		})

		When("IPv4 mapping is defined for a host", func() {
			It("defined ipv4 query should be resolved", func() {
				Expect(sut.Resolve(ctx, newRequest("ipv4host.", A))).
					Should(
						SatisfyAll(
							HaveResponseType(ResponseTypeHOSTSFILE),
							HaveReturnCode(dns.RcodeSuccess),
							BeDNSRecord("ipv4host.", A, "192.168.2.1"),
							HaveTTL(BeNumerically("==", TTL)),
						))
			})
			It("defined ipv4 query for alias should be resolved", func() {
				Expect(sut.Resolve(ctx, newRequest("router2.", A))).
					Should(
						SatisfyAll(
							HaveResponseType(ResponseTypeHOSTSFILE),
							HaveReturnCode(dns.RcodeSuccess),
							BeDNSRecord("router2.", A, "10.0.0.1"),
							HaveTTL(BeNumerically("==", TTL)),
						))
			})
			It("ipv4 query should return NOERROR and empty result", func() {
				Expect(sut.Resolve(ctx, newRequest("does.not.exist.", A))).
					Should(
						SatisfyAll(
							HaveNoAnswer(),
							HaveReturnCode(dns.RcodeSuccess),
							HaveResponseType(ResponseTypeRESOLVED),
						))
			})
		})

		When("IPv6 mapping is defined for a host", func() {
			It("defined ipv6 query should be resolved", func() {
				Expect(sut.Resolve(ctx, newRequest("ipv6host.", AAAA))).
					Should(
						SatisfyAll(
							HaveResponseType(ResponseTypeHOSTSFILE),
							HaveReturnCode(dns.RcodeSuccess),
							BeDNSRecord("ipv6host.", AAAA, "faaf:faaf:faaf:faaf::1"),
							HaveTTL(BeNumerically("==", TTL)),
						))
			})
			It("ipv6 query should return NOERROR and empty result", func() {
				Expect(sut.Resolve(ctx, newRequest("does.not.exist.", AAAA))).
					Should(
						SatisfyAll(
							HaveNoAnswer(),
							HaveReturnCode(dns.RcodeSuccess),
							HaveResponseType(ResponseTypeRESOLVED),
						))
			})
		})

		When("the domain is not known", func() {
			It("calls the next resolver", func() {
				resp, err = sut.Resolve(ctx, newRequest("not-in-hostsfile.tld.", A))
				Expect(err).Should(Succeed())
				Expect(resp).ShouldNot(HaveResponseType(ResponseTypeHOSTSFILE))
				m.AssertExpectations(GinkgoT())
			})
		})

		When("the question type is not handled", func() {
			It("calls the next resolver", func() {
				resp, err = sut.Resolve(ctx, newRequest("localhost.", MX))
				Expect(err).Should(Succeed())
				Expect(resp).ShouldNot(HaveResponseType(ResponseTypeHOSTSFILE))
				m.AssertExpectations(GinkgoT())
			})
		})

		When("Reverse DNS request is received", func() {
			It("should resolve the defined domain name", func() {
				By("ipv4 with one hostname", func() {
					Expect(sut.Resolve(ctx, newRequest("2.0.0.10.in-addr.arpa.", PTR))).
						Should(
							SatisfyAll(
								HaveResponseType(ResponseTypeHOSTSFILE),
								HaveReturnCode(dns.RcodeSuccess),
								BeDNSRecord("2.0.0.10.in-addr.arpa.", PTR, "router3."),
								HaveTTL(BeNumerically("==", TTL)),
							))
				})
				By("ipv4 with aliases", func() {
					Expect(sut.Resolve(ctx, newRequest("1.0.0.10.in-addr.arpa.", PTR))).
						Should(
							SatisfyAll(
								HaveResponseType(ResponseTypeHOSTSFILE),
								HaveReturnCode(dns.RcodeSuccess),
								WithTransform(ToAnswer, ContainElements(
									BeDNSRecord("1.0.0.10.in-addr.arpa.", PTR, "router0."),
									BeDNSRecord("1.0.0.10.in-addr.arpa.", PTR, "router1."),
									BeDNSRecord("1.0.0.10.in-addr.arpa.", PTR, "router2."),
								)),
							))
				})
				By("ipv6", func() {
					Expect(sut.Resolve(ctx,
						newRequest("1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.f.a.a.f.f.a.a.f.f.a.a.f.f.a.a.f.ip6.arpa.", PTR)),
					).
						Should(
							SatisfyAll(
								HaveResponseType(ResponseTypeHOSTSFILE),
								HaveReturnCode(dns.RcodeSuccess),
								WithTransform(ToAnswer, ContainElements(
									BeDNSRecord("1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.f.a.a.f.f.a.a.f.f.a.a.f.f.a.a.f.ip6.arpa.",
										PTR, "ipv6host."),
									BeDNSRecord("1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.f.a.a.f.f.a.a.f.f.a.a.f.f.a.a.f.ip6.arpa.",
										PTR, "ipv6host.local.lan."),
								)),
							))
				})
			})

			It("should ignore invalid PTR", func() {
				resp, err = sut.Resolve(ctx, newRequest("2.0.0.10.in-addr.fail.arpa.", PTR))
				Expect(err).Should(Succeed())
				Expect(resp).ShouldNot(HaveResponseType(ResponseTypeHOSTSFILE))
				m.AssertExpectations(GinkgoT())
			})

			When("filterLoopback is true", func() {
				It("calls the next resolver", func() {
					resp, err = sut.Resolve(ctx, newRequest("1.0.0.127.in-addr.arpa.", PTR))
					Expect(err).Should(Succeed())
					Expect(resp).ShouldNot(HaveResponseType(ResponseTypeHOSTSFILE))
					m.AssertExpectations(GinkgoT())
				})
			})

			When("the IP is not known", func() {
				It("calls the next resolver", func() {
					resp, err = sut.Resolve(ctx, newRequest("255.255.255.255.in-addr.arpa.", PTR))
					Expect(err).Should(Succeed())
					Expect(resp).ShouldNot(HaveResponseType(ResponseTypeHOSTSFILE))
					m.AssertExpectations(GinkgoT())
				})
			})

			When("filterLoopback is false", func() {
				BeforeEach(func() {
					sutConfig.FilterLoopback = false
				})

				It("resolve the defined domain name", func() {
					Expect(sut.Resolve(ctx, newRequest("1.1.0.127.in-addr.arpa.", PTR))).
						Should(
							SatisfyAll(
								HaveResponseType(ResponseTypeHOSTSFILE),
								HaveReturnCode(dns.RcodeSuccess),
								WithTransform(ToAnswer, ContainElements(
									BeDNSRecord("1.1.0.127.in-addr.arpa.", PTR, "localhost2."),
									BeDNSRecord("1.1.0.127.in-addr.arpa.", PTR, "localhost2.local.lan."),
								)),
							))
				})
			})
		})
	})

	Describe("Delegating to next resolver", func() {
		When("no hosts file is provided", func() {
			It("should delegate to next resolver", func() {
				_, err = sut.Resolve(ctx, newRequest("example.com.", A))
				Expect(err).Should(Succeed())
				// delegate was executed
				m.AssertExpectations(GinkgoT())
			})
		})
	})
})

func writeHostFile(tmpDir *TmpFolder) *TmpFile {
	return tmpDir.CreateStringFile("hosts.txt",
		"# Random comment",
		"127.0.0.1               localhost",
		"127.0.1.1               localhost2  localhost2.local.lan",
		"::1                     localhost",
		"# Two empty lines to follow",
		"",
		"",
		"faaf:faaf:faaf:faaf::1  ipv6host    ipv6host.local.lan",
		"192.168.2.1             ipv4host    ipv4host.local.lan",
		"faaf:faaf:faaf:faaf::2  dualhost    dualhost.local.lan",
		"192.168.2.2             dualhost    dualhost.local.lan",
		"10.0.0.1                router0 router1 router2",
		"10.0.0.2                router3     # Another comment",
		"10.0.0.3                router4#comment without a space",
		"10.0.0.4                            # Invalid entry",
		"300.300.300.300         invalid4    # Invalid IPv4",
		"abcd:efgh:ijkl::1       invalid6    # Invalid IPv6",
		"1.2.3.4                 localhost", // localhost name but not localhost IP

		// from https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts
		"fe80::1%lo0             localhost", // interface name
	)
}
