// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause

//go:build linux

package dns

import (
	"context"
	"fmt"
	"net/netip"
	"os"
	"path/filepath"
	"testing"
	"testing/synctest"

	"github.com/illarion/gonotify/v3"

	"tailscale.com/util/dnsname"
	"tailscale.com/util/eventbus/eventbustest"
)

func TestDNSTrampleRecovery(t *testing.T) {
	HookWatchFile.Set(watchFile)
	synctest.Test(t, func(t *testing.T) {
		tmp := t.TempDir()
		if err := os.MkdirAll(filepath.Join(tmp, "etc"), 0700); err != nil {
			t.Fatal(err)
		}
		const resolvPath = "/etc/resolv.conf"
		fs := directFS{prefix: tmp}
		readFile := func(t *testing.T, path string) string {
			t.Helper()
			b, err := fs.ReadFile(path)
			if err != nil {
				t.Errorf("Reading DNS config: %v", err)
			}
			return string(b)
		}

		bus := eventbustest.NewBus(t)
		eventbustest.LogAllEvents(t, bus)
		m := newDirectManagerOnFS(t.Logf, nil, bus, fs)
		defer m.Close()

		if err := m.SetDNS(OSConfig{
			Nameservers:   []netip.Addr{netip.MustParseAddr("8.8.8.8"), netip.MustParseAddr("8.8.4.4")},
			SearchDomains: []dnsname.FQDN{"ts.net.", "ts-dns.test."},
			MatchDomains:  []dnsname.FQDN{"ignored."},
		}); err != nil {
			t.Fatal(err)
		}

		const want = `# resolv.conf(5) file generated by tailscale
# For more info, see https://tailscale.com/s/resolvconf-overwrite
# DO NOT EDIT THIS FILE BY HAND -- CHANGES WILL BE OVERWRITTEN

nameserver 8.8.8.8
nameserver 8.8.4.4
search ts.net ts-dns.test
`
		if got := readFile(t, resolvPath); got != want {
			t.Fatalf("resolv.conf:\n%s, want:\n%s", got, want)
		}

		tw := eventbustest.NewWatcher(t, bus)

		const trample = "Hvem er det som tramper på min bro?"
		if err := fs.WriteFile(resolvPath, []byte(trample), 0644); err != nil {
			t.Fatal(err)
		}
		synctest.Wait()

		if err := eventbustest.Expect(tw, eventbustest.Type[TrampleDNS]()); err != nil {
			t.Errorf("did not see trample event: %s", err)
		}
	})
}

// watchFile is generally copied from linuxtrample, but cancels the context
// after the first call to cb() after the first trample to end the test.
func watchFile(ctx context.Context, dir, filename string, cb func()) error {
	ctx, cancel := context.WithCancel(ctx)
	defer cancel()

	const events = gonotify.IN_ATTRIB |
		gonotify.IN_CLOSE_WRITE |
		gonotify.IN_CREATE |
		gonotify.IN_DELETE |
		gonotify.IN_MODIFY |
		gonotify.IN_MOVE

	watcher, err := gonotify.NewDirWatcher(ctx, events, dir)
	if err != nil {
		return fmt.Errorf("NewDirWatcher: %w", err)
	}

	for {
		select {
		case event := <-watcher.C:
			if event.Name == filename {
				cb()
				cancel()
			}
		case <-ctx.Done():
			return ctx.Err()
		}
	}
}
