// Copyright 2019 The Outline Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package ipmap

import (
	"context"
	"errors"
	"net"
	"net/netip"
	"sync/atomic"
	"testing"
)

func TestGetTwice(t *testing.T) {
	m := NewIPMap()
	a := m.Get("example")
	b := m.Get("example")
	if a == b {
		t.Error("Matched Get returned different objects")
	}
}

func TestGetInvalid(t *testing.T) {
	m := NewIPMap()
	s := m.Get("example")
	if !s.Empty() {
		t.Error("Invalid name should result in an empty set")
	}
	if len(s.Addrs()) != 0 {
		t.Error("Empty set should be empty")
	}
}

func TestGetDomain(t *testing.T) {
	m := NewIPMap()
	s := m.Get("www.google.com")
	if s.Empty() {
		t.Error("Google lookup failed")
	}
	ips := s.Addrs()
	if len(ips) == 0 {
		t.Fatal("IP set is empty")
	} else if !ips[0].IsValid() {
		t.Error("nil IP in set")
	}
}

func TestGetIP(t *testing.T) {
	m := NewIPMap()
	s := m.Get("192.0.2.1")
	if s.Empty() {
		t.Error("IP parsing failed")
	}
	ips := s.Addrs()
	if len(ips) != 1 {
		t.Errorf("Wrong IP set size %d", len(ips))
	} else if ips[0].Unmap().String() != "192.0.2.1" {
		t.Error("Wrong IP")
	}
}

func TestAddDomain(t *testing.T) {
	m := NewIPMap()
	s := m.Get("example")
	s.add("www.google.com")
	if s.Empty() {
		t.Error("Google lookup failed")
	}
	ips := s.Addrs()
	if len(ips) == 0 {
		t.Fatal("IP set is empty")
	} else if !ips[0].IsValid() {
		t.Error("nil IP in set")
	}
}
func TestAddIP(t *testing.T) {
	m := NewIPMap()
	s := m.Get("example")
	s.add("192.0.2.1")
	ips := s.Addrs()
	if len(ips) != 1 {
		t.Errorf("Wrong IP set size %d", len(ips))
	} else if ips[0].Unmap().String() != "192.0.2.1" {
		t.Error("Wrong IP")
	}
}

func TestConfirmed(t *testing.T) {
	m := NewIPMap()
	fqdn := "www.google.com"
	s := m.Get(fqdn)
	if s.Confirmed().IsValid() {
		t.Error("Confirmed should start out nil")
	}

	ips := s.Addrs()
	if len(ips) == 0 {
		t.Fatalf("Empty IPSet for %s", fqdn)
		return
	}
	s.Confirm(ips[0])
	if ips[0].Compare(s.Confirmed()) != 0 {
		t.Error("Confirmation failed")
	}

	s.Disconfirm(ips[0])
	if s.Confirmed().IsValid() {
		t.Error("Confirmed should now be nil")
	}
}

func TestConfirmNew(t *testing.T) {
	m := NewIPMap()
	s := m.Get("example")
	s.add("192.0.2.1")
	// Confirm a new address.
	s.Confirm(netip.MustParseAddr("192.0.2.2"))
	if !s.Confirmed().IsValid() || s.Confirmed().String() != "192.0.2.2" {
		t.Error("Confirmation failed")
	}
	ips := s.Addrs()
	if len(ips) != 2 {
		t.Errorf("New address not added to the set; %v", ips)
	}
}

func TestDisconfirmMismatch(t *testing.T) {
	m := NewIPMap()
	fqdn := "www.google.com"
	s := m.Get(fqdn)
	ips := s.Addrs()
	if len(ips) == 0 {
		t.Fatalf("Empty IPSet for %s", fqdn)
		return
	}
	s.Confirm(ips[0])

	// Make a copy
	otherIP := netip.MustParseAddr(ips[0].String())
	// Alter it
	otherIP = otherIP.Next()
	// Disconfirm.  This should have no effect because otherIP
	// is not the confirmed IP.
	s.Disconfirm(otherIP)

	if ips[0].Compare(s.Confirmed()) != 0 {
		t.Error("Mismatched disconfirmation")
	}
}

type fakeResolver struct {
	*net.Resolver
}

func (r fakeResolver) Lookup([]byte, ...string) ([]byte, error) {
	return nil, errors.New("not implemented")
}

func (r fakeResolver) LookupFor([]byte, string) ([]byte, error) {
	return nil, errors.New("not implemented")
}

func (r fakeResolver) LookupNetIP(_ context.Context, _, _ string) ([]netip.Addr, error) {
	return nil, errors.New("not implemented")
}

func (r fakeResolver) LookupNetIPFor(_ context.Context, _, _, _ string) ([]netip.Addr, error) {
	return nil, errors.New("not implemented")
}

func (r fakeResolver) LookupNetIPOn(_ context.Context, _, _ string, _ ...string) ([]netip.Addr, error) {
	return nil, errors.New("not implemented")
}

func TestResolver(t *testing.T) {
	var dialCount int32
	r := &net.Resolver{
		PreferGo: true,
		Dial: func(context context.Context, network, address string) (net.Conn, error) {
			atomic.AddInt32(&dialCount, 1)
			return nil, errors.New("Fake dialer")
		},
	}
	resolver := &fakeResolver{r}
	m := NewIPMapFor(resolver)
	s := m.Get("www.google.com")
	if !s.Empty() {
		t.Error("Google lookup should have failed due to fake dialer")
	}
	if atomic.LoadInt32(&dialCount) == 0 {
		t.Error("Fake dialer didn't run")
	}
}
