// Unit tests for the dns package.
package dns

import (
	"net"
	"os"
	"testing"
)

// ---- ParsePushReply ----------------------------------------------------------

func TestParsePushReply_DNS(t *testing.T) {
	msg := "PUSH_REPLY,ifconfig 10.8.0.6 10.8.0.5," +
		"dhcp-option DNS 10.8.0.1,dhcp-option DNS 8.8.8.8," +
		"dhcp-option DOMAIN corp.example.com\x00"

	cfg, err := ParsePushReply(msg)
	if err != nil {
		t.Fatalf("ParsePushReply: %v", err)
	}

	if len(cfg.Servers) != 2 {
		t.Fatalf("expected 2 DNS servers, got %d", len(cfg.Servers))
	}
	if !cfg.Servers[0].Equal(net.ParseIP("10.8.0.1")) {
		t.Errorf("server[0]: got %s, want 10.8.0.1", cfg.Servers[0])
	}
	if !cfg.Servers[1].Equal(net.ParseIP("8.8.8.8")) {
		t.Errorf("server[1]: got %s, want 8.8.8.8", cfg.Servers[1])
	}
	if len(cfg.SearchDomains) != 1 || cfg.SearchDomains[0] != "corp.example.com" {
		t.Errorf("search domains: got %v, want [corp.example.com]", cfg.SearchDomains)
	}
}

func TestParsePushReply_NoDNS(t *testing.T) {
	msg := "PUSH_REPLY,ifconfig 10.8.0.6 10.8.0.5,cipher AES-256-GCM"
	cfg, err := ParsePushReply(msg)
	if err != nil {
		t.Fatalf("ParsePushReply: %v", err)
	}
	if len(cfg.Servers) != 0 {
		t.Errorf("expected 0 servers, got %d", len(cfg.Servers))
	}
}

func TestParsePushReply_BadDNS(t *testing.T) {
	_, err := ParsePushReply("PUSH_REPLY,dhcp-option DNS not-an-ip")
	if err == nil {
		t.Error("expected error for invalid DNS IP")
	}
}

// ---- ApplyResolvConf / RestoreResolvConf ------------------------------------

func TestApplyResolvConf(t *testing.T) {
	// Use a temp file to avoid touching the real /etc/resolv.conf.
	tmp, err := os.CreateTemp(t.TempDir(), "resolv.conf")
	if err != nil {
		t.Fatal(err)
	}
	tmp.Close()

	orig := ResolvConfPath
	ResolvConfPath = tmp.Name()
	defer func() { ResolvConfPath = orig }()

	cfg := &Config{
		Servers:       []net.IP{net.ParseIP("10.8.0.1"), net.ParseIP("8.8.8.8")},
		SearchDomains: []string{"vpn.example.com"},
	}
	if err := ApplyResolvConf(cfg); err != nil {
		t.Fatalf("ApplyResolvConf: %v", err)
	}

	data, err := os.ReadFile(tmp.Name())
	if err != nil {
		t.Fatal(err)
	}
	got := string(data)
	t.Logf("resolv.conf:\n%s", got)

	for _, want := range []string{
		"# Generated by go-openlawsvpn",
		"nameserver 10.8.0.1",
		"nameserver 8.8.8.8",
		"search vpn.example.com",
	} {
		if !contains(got, want) {
			t.Errorf("missing %q in resolv.conf", want)
		}
	}
}

func TestBackupAndRestore(t *testing.T) {
	dir := t.TempDir()
	resolvFile := dir + "/resolv.conf"
	backupFile := dir + "/resolv.conf.bak"

	orig := ResolvConfPath
	ResolvConfPath = resolvFile
	defer func() { ResolvConfPath = orig }()

	// Write an "original" resolv.conf.
	original := "nameserver 1.1.1.1\n"
	if err := os.WriteFile(resolvFile, []byte(original), 0644); err != nil {
		t.Fatal(err)
	}

	// Back it up.
	if err := BackupResolvConf(backupFile); err != nil {
		t.Fatalf("BackupResolvConf: %v", err)
	}

	// Overwrite with VPN config.
	vpnCfg := &Config{Servers: []net.IP{net.ParseIP("10.8.0.1")}}
	if err := ApplyResolvConf(vpnCfg); err != nil {
		t.Fatalf("ApplyResolvConf: %v", err)
	}

	// Restore from backup.
	if err := RestoreResolvConf(backupFile); err != nil {
		t.Fatalf("RestoreResolvConf: %v", err)
	}

	data, err := os.ReadFile(resolvFile)
	if err != nil {
		t.Fatal(err)
	}
	if string(data) != original {
		t.Errorf("restored content: got %q, want %q", string(data), original)
	}

	// Backup file should be removed after restore.
	if _, err := os.Stat(backupFile); !os.IsNotExist(err) {
		t.Error("backup file should be removed after RestoreResolvConf")
	}
}

// TestRevertResolvConf verifies that Revert(BackendResolvConf) restores the
// original file content and removes the backup.  This exercises the full
// Apply→Revert cycle that cleanup() uses on disconnect.
func TestRevertResolvConf(t *testing.T) {
	dir := t.TempDir()
	resolvFile := dir + "/resolv.conf"
	backupFile := dir + "/resolv.conf.bak"

	orig := ResolvConfPath
	ResolvConfPath = resolvFile
	defer func() { ResolvConfPath = orig }()

	original := "nameserver 1.1.1.1\n"
	if err := os.WriteFile(resolvFile, []byte(original), 0644); err != nil {
		t.Fatal(err)
	}

	if err := BackupResolvConf(backupFile); err != nil {
		t.Fatalf("BackupResolvConf: %v", err)
	}
	vpnCfg := &Config{Servers: []net.IP{net.ParseIP("10.8.0.1")}}
	if err := ApplyResolvConf(vpnCfg); err != nil {
		t.Fatalf("ApplyResolvConf: %v", err)
	}

	if err := Revert(BackendResolvConf, "tun0", backupFile); err != nil {
		t.Fatalf("Revert: %v", err)
	}

	data, err := os.ReadFile(resolvFile)
	if err != nil {
		t.Fatal(err)
	}
	if string(data) != original {
		t.Errorf("after Revert: got %q, want %q", string(data), original)
	}
	if _, err := os.Stat(backupFile); !os.IsNotExist(err) {
		t.Error("backup file should be removed after Revert")
	}
}

// TestRevertResolvConfEmptyBackup documents that Revert with an empty backup
// path is a no-op — the original resolv.conf is not restored.  This was the
// behaviour before the fix (c.dnsBackup was always ""); openNativeTUN now
// always provides a non-empty path.
func TestRevertResolvConfEmptyBackup(t *testing.T) {
	dir := t.TempDir()
	resolvFile := dir + "/resolv.conf"

	orig := ResolvConfPath
	ResolvConfPath = resolvFile
	defer func() { ResolvConfPath = orig }()

	if err := os.WriteFile(resolvFile, []byte("nameserver 1.1.1.1\n"), 0644); err != nil {
		t.Fatal(err)
	}
	vpnCfg := &Config{Servers: []net.IP{net.ParseIP("10.8.0.1")}}
	if err := ApplyResolvConf(vpnCfg); err != nil {
		t.Fatalf("ApplyResolvConf: %v", err)
	}

	// Empty backupPath — Revert must not crash and must leave the VPN config in place.
	if err := Revert(BackendResolvConf, "tun0", ""); err != nil {
		t.Fatalf("Revert with empty backupPath: %v", err)
	}

	data, err := os.ReadFile(resolvFile)
	if err != nil {
		t.Fatal(err)
	}
	if !contains(string(data), "10.8.0.1") {
		t.Error("expected VPN nameserver still present (no backup to restore from)")
	}
}

func TestIsManaged(t *testing.T) {
	dir := t.TempDir()
	path := dir + "/resolv.conf"

	// Managed file.
	os.WriteFile(path, []byte("# Generated by go-openlawsvpn\nnameserver 10.0.0.1\n"), 0644)
	managed, err := IsManaged(path)
	if err != nil {
		t.Fatal(err)
	}
	if !managed {
		t.Error("expected IsManaged=true for go-openlawsvpn-generated file")
	}

	// Non-managed file.
	os.WriteFile(path, []byte("# system resolv.conf\nnameserver 1.1.1.1\n"), 0644)
	managed, err = IsManaged(path)
	if err != nil {
		t.Fatal(err)
	}
	if managed {
		t.Error("expected IsManaged=false for non-generated file")
	}
}

func contains(s, substr string) bool {
	return len(s) >= len(substr) && (s == substr ||
		len(s) > 0 && findSubstr(s, substr))
}

func findSubstr(s, sub string) bool {
	for i := 0; i <= len(s)-len(sub); i++ {
		if s[i:i+len(sub)] == sub {
			return true
		}
	}
	return false
}
