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

//go:build linux

package dns

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

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

func TestDNSTrampleRecovery(t *testing.T) {
	t.Cleanup(HookWatchFile.SetForTest(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 a test implementation of the file watcher that uses a timer
// instead of inotify. Real inotify (gonotify.NewDirWatcher) creates goroutines
// that block on real syscalls, which don't work inside synctest's fake-time
// bubble. Instead, we use a one-shot timer that synctest.Wait() will advance,
// triggering a callback to check for file trampling.
func watchFile(ctx context.Context, dir, filename string, cb func()) error {
	timer := time.NewTimer(time.Millisecond)
	defer timer.Stop()
	select {
	case <-ctx.Done():
		return ctx.Err()
	case <-timer.C:
		cb()
	}
	<-ctx.Done()
	return ctx.Err()
}
