package resolvconf

import (
	"os"
	"path/filepath"
	"testing"

	"github.com/opencontainers/runtime-spec/specs-go"
	"github.com/stretchr/testify/assert"
)

const resolv1 = `nameserver 1.1.1.1
`

const resolv2 = `search example.com
nameserver 1.1.1.1
options edns0
`

func TestNew(t *testing.T) {
	tests := []struct {
		name            string
		baseContent     string
		nameservers     []string
		options         []string
		searches        []string
		ipv6            bool
		hostns          bool
		keepHostServers bool
		want            string
	}{
		{
			name:        "simple resolv.conf",
			baseContent: resolv1,
			want:        resolv1,
		},
		{
			name:        "simple resolv.conf with search and options",
			baseContent: resolv2,
			want:        resolv2,
		},
		{
			name:        "simple resolv.conf with comments",
			baseContent: "#some comment\n" + resolv2,
			want:        resolv2,
		},
		{
			name:        "overwrite default nameservers",
			baseContent: resolv2,
			nameservers: []string{"1.2.3.4", "5.6.7.8"},
			want:        "search example.com\nnameserver 1.2.3.4\nnameserver 5.6.7.8\noptions edns0\n",
		},
		{
			name:        "overwrite default options",
			baseContent: resolv2,
			options:     []string{"ndots:2"},
			want:        "search example.com\nnameserver 1.1.1.1\noptions ndots:2\n",
		},
		{
			name:        "overwrite default searches",
			baseContent: resolv2,
			searches:    []string{"test.com"},
			want:        "search test.com\nnameserver 1.1.1.1\noptions edns0\n",
		},
		{
			name:        "dot in searches should unset all search domains",
			baseContent: resolv2,
			searches:    []string{"."},
			want:        "nameserver 1.1.1.1\noptions edns0\n",
		},
		{
			name:            "dot in searches should unset all search domains even with keepHostServers",
			baseContent:     resolv2,
			searches:        []string{"."},
			keepHostServers: true,
			want:            "nameserver 1.1.1.1\noptions edns0\n",
		},
		{
			name:        "overwrite all",
			baseContent: resolv2,
			nameservers: []string{"1.2.3.4", "5.6.7.8"},
			options:     []string{"ndots:2"},
			searches:    []string{"test.com"},
			want:        "search test.com\nnameserver 1.2.3.4\nnameserver 5.6.7.8\noptions ndots:2\n",
		},
		{
			name:        "overwrite all and unset search",
			baseContent: resolv2,
			nameservers: []string{"1.2.3.4", "5.6.7.8"},
			options:     []string{"ndots:2"},
			searches:    []string{"."},
			want:        "nameserver 1.2.3.4\nnameserver 5.6.7.8\noptions ndots:2\n",
		},
		{
			name:            "set all and keep host server",
			baseContent:     resolv2,
			nameservers:     []string{"1.2.3.4", "5.6.7.8"},
			options:         []string{"ndots:2"},
			searches:        []string{"test.com"},
			keepHostServers: true,
			want:            "search test.com example.com\nnameserver 1.2.3.4\nnameserver 5.6.7.8\nnameserver 1.1.1.1\noptions ndots:2 edns0\n",
		},
		{
			name:        "localhost nameservers should be filtered and use defaults instead",
			baseContent: "nameserver 127.0.0.1\nnameserver ::1\n",
			want:        "nameserver 8.8.8.8\nnameserver 8.8.4.4\n",
		},
		{
			name:        "localhost nameservers should not be filtered with hostns",
			baseContent: "nameserver 127.0.0.1\nnameserver ::1\n",
			hostns:      true,
			want:        "nameserver 127.0.0.1\nnameserver ::1\n",
		},
		{
			name:        "ipv6 nameservers should be filtered when ipv6 is not set",
			baseContent: "nameserver 1.1.1.1\nnameserver fd::1\n",
			want:        "nameserver 1.1.1.1\n",
		},
		{
			name:        "ipv6 nameservers should not be filtered when ipv6 is set",
			baseContent: "nameserver 1.1.1.1\nnameserver fd::1\n",
			ipv6:        true,
			want:        "nameserver 1.1.1.1\nnameserver fd::1\n",
		},
		{
			name:        "ipv6 link local must always be filtered when netns is private",
			baseContent: "nameserver 1.1.1.1\nnameserver fe80::1%eth1\nnameserver fd::1\n",
			ipv6:        true,
			want:        "nameserver 1.1.1.1\nnameserver fd::1\n",
		},
		{
			name:        "ipv6 link local must not be filtered when netns is host",
			baseContent: "nameserver 1.1.1.1\nnameserver fe80::1%eth1\nnameserver fd::1\n",
			hostns:      true,
			want:        "nameserver 1.1.1.1\nnameserver fe80::1%eth1\nnameserver fd::1\n",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			base := filepath.Join(t.TempDir(), "resolv.conf")
			target := filepath.Join(t.TempDir(), "new-resolv.conf")
			err := os.WriteFile(base, []byte(tt.baseContent), 0o644)
			assert.NoError(t, err, "write tmp resolv.conf")
			var namespaces []specs.LinuxNamespace
			if !tt.hostns {
				namespaces = []specs.LinuxNamespace{
					{Type: specs.NetworkNamespace},
				}
			}
			err = New(&Params{
				Path:            target,
				Nameservers:     tt.nameservers,
				Searches:        tt.searches,
				Options:         tt.options,
				IPv6Enabled:     tt.ipv6,
				KeepHostServers: tt.keepHostServers,
				Namespaces:      namespaces,
				resolvConfPath:  base,
			})
			assert.NoError(t, err, "New()")
			content, err := os.ReadFile(target)
			assert.NoError(t, err, "read tmp resolv.conf)")
			assert.Equal(t, tt.want, string(content), "expected resolv.conf does not match")
		})
	}
}

func TestAdd(t *testing.T) {
	tests := []struct {
		name        string
		content     string
		nameservers []string
		want        string
	}{
		{
			name:        "add single nameserver",
			content:     resolv1,
			nameservers: []string{"1.2.3.4"},
			want:        "nameserver 1.2.3.4\n" + resolv1,
		},
		{
			name:        "add single nameserver with search and options",
			content:     resolv2,
			nameservers: []string{"1.2.3.4"},
			want: `search example.com
nameserver 1.2.3.4
nameserver 1.1.1.1
options edns0
`,
		},
		{
			name:        "add three nameservers",
			content:     resolv1,
			nameservers: []string{"1.2.3.4", "2.3.4.5", "3.4.5.6"},
			want:        "nameserver 1.2.3.4\nnameserver 2.3.4.5\nnameserver 3.4.5.6\n" + resolv1,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			resolvPath := filepath.Join(t.TempDir(), "resolv.conf")
			err := os.WriteFile(resolvPath, []byte(tt.content), 0o644)
			assert.NoError(t, err, "write tmp resolv.conf")
			err = Add(resolvPath, tt.nameservers)
			assert.NoError(t, err, "Add()")
			content, err := os.ReadFile(resolvPath)
			assert.NoError(t, err, "read tmp resolv.conf)")
			assert.Equal(t, tt.want, string(content), "expected resolv.conf does not match")
		})
	}
}

func TestRemove(t *testing.T) {
	tests := []struct {
		name        string
		content     string
		nameservers []string
		want        string
	}{
		{
			name:        "remove single nameserver",
			content:     resolv1,
			nameservers: []string{"1.1.1.1"},
			want:        "",
		},
		{
			name:        "remove single nameserver with search and options",
			content:     resolv2,
			nameservers: []string{"1.1.1.1"},
			want: `search example.com
options edns0
`,
		},
		{
			name:        "remove three nameservers",
			content:     "nameserver 1.2.3.4\nnameserver 2.3.4.5\nnameserver 3.4.5.6\n" + resolv1,
			nameservers: []string{"1.2.3.4", "2.3.4.5", "3.4.5.6"},
			want:        resolv1,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			resolvPath := filepath.Join(t.TempDir(), "resolv.conf")
			err := os.WriteFile(resolvPath, []byte(tt.content), 0o644)
			assert.NoError(t, err, "write tmp resolv.conf")
			err = Remove(resolvPath, tt.nameservers)
			assert.NoError(t, err, "Remove()")
			content, err := os.ReadFile(resolvPath)
			assert.NoError(t, err, "read tmp resolv.conf)")
			assert.Equal(t, tt.want, string(content), "expected resolv.conf does not match")
		})
	}
}
