// Copyright © 2016 Asteris, LLC
//
// 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 owner_test

import (
	"os"
	"os/user"
	"path/filepath"
	"strconv"
	"syscall"
	"testing"
	"time"

	"github.com/stretchr/testify/mock"
)

// any aliases mock.Anything for terseness
var any = mock.Anything

// MockOS mocks OSProxy
type MockOS struct {
	mock.Mock
	walkArgs []*walkArgs
}

// Walk mocks Walk
func (m *MockOS) Walk(root string, walkFunc filepath.WalkFunc) error {
	args := m.Called(root, walkFunc)
	for _, args := range m.walkArgs {
		walkFunc(args.Path, args.Info, args.Err)
	}
	return args.Error(0)
}

// Stat mocks Stat
func (m *MockOS) Stat(path string) (os.FileInfo, error) {
	args := m.Called(path)
	return args.Get(0).(os.FileInfo), args.Error(1)
}

// Chown mocks Chown
func (m *MockOS) Chown(path string, uid, gid int) error {
	args := m.Called(path, uid, gid)
	return args.Error(0)
}

// GetUID mocks GetUID
func (m *MockOS) GetUID(path string) (int, error) {
	args := m.Called(path)
	return args.Int(0), args.Error(1)
}

// GetGID mocks GetGID
func (m *MockOS) GetGID(path string) (int, error) {
	args := m.Called(path)
	return args.Int(0), args.Error(1)
}

// LookupGroupID mocks LookupGroupID
func (m *MockOS) LookupGroupID(path string) (*user.Group, error) {
	args := m.Called(path)
	return args.Get(0).(*user.Group), args.Error(1)
}

// LookupGroup mocks LookupGroup
func (m *MockOS) LookupGroup(path string) (*user.Group, error) {
	args := m.Called(path)
	return args.Get(0).(*user.Group), args.Error(1)
}

// LookupID mocks LookupID
func (m *MockOS) LookupID(path string) (*user.User, error) {
	args := m.Called(path)
	return args.Get(0).(*user.User), args.Error(1)
}

// Lookup mocks Lookup
func (m *MockOS) Lookup(path string) (*user.User, error) {
	args := m.Called(path)
	return args.Get(0).(*user.User), args.Error(1)
}

func fakeUser(uid, gid, name string) *user.User {
	return &user.User{
		Uid:      uid,
		Gid:      gid,
		Username: name,
		Name:     name,
		HomeDir:  "~" + name,
	}
}

func fakeGroup(gid, name string) *user.Group {
	return &user.Group{Gid: gid, Name: name}
}

type ownershipRecord struct {
	Path      string
	User      *user.User
	Group     *user.Group
	FileSize  int64
	FileMode  os.FileMode
	FileIsDir bool
}

func (o ownershipRecord) Name() string {
	return o.Path
}

func (o ownershipRecord) Size() int64 {
	return o.FileSize
}

func (o ownershipRecord) Mode() os.FileMode {
	return o.FileMode
}

func (o ownershipRecord) ModTime() time.Time {
	return time.Now()
}

func (o ownershipRecord) IsDir() bool {
	return o.FileIsDir
}

func (o ownershipRecord) Sys() interface{} {
	uid, _ := strconv.Atoi(o.User.Uid)
	gid, _ := strconv.Atoi(o.User.Gid)
	return &syscall.Stat_t{
		Uid:  uint32(uid),
		Gid:  uint32(gid),
		Size: o.FileSize,
	}
}

func (o *ownershipRecord) ToWalkArgs() *walkArgs {
	return &walkArgs{
		Path: o.Path,
		Err:  nil,
		Info: o,
	}
}

type walkArgs struct {
	Path string
	Info os.FileInfo
	Err  error
}

var (
	rootUser = &user.User{
		Uid:      "0",
		Gid:      "0",
		Username: "root",
		Name:     "root",
		HomeDir:  "/root",
	}
	rootGroup = &user.Group{
		Gid:  "0",
		Name: "root",
	}
	fakeStat = &ownershipRecord{
		Path:      "fake-path",
		User:      rootUser,
		Group:     rootGroup,
		FileSize:  0,
		FileMode:  0777,
		FileIsDir: false,
	}
)

func makeOwned(path, username, uid, groupname, gid string) ownershipRecord {
	return ownershipRecord{
		Path: path,
		User: &user.User{
			Uid:      uid,
			Gid:      gid,
			Username: username,
			Name:     username,
			HomeDir:  "~" + username,
		},
		Group: &user.Group{
			Gid:  gid,
			Name: groupname,
		},
	}
}

func makeOwnedFull(path, username, uid, groupname, gid string, isDir bool, size int64, mode os.FileMode) ownershipRecord {
	o := makeOwned(path, username, uid, groupname, gid)
	o.FileSize = size
	o.FileMode = mode
	o.FileIsDir = isDir
	return o
}

func newMockOS(ownedFiles []ownershipRecord,
	users []*user.User,
	groups []*user.Group,
	defaultUser *user.User,
	defaultGroup *user.Group,
) *MockOS {
	m := &MockOS{}

	for _, o := range ownedFiles {
		m.walkArgs = append(m.walkArgs, o.ToWalkArgs())
	}
	if defaultUser == nil {
		defaultUser = rootUser
	}
	if defaultGroup == nil {
		defaultGroup = rootGroup
	}

	m.On("Walk", any, any).Return(nil)
	m.On("Chown", any, any, any).Return(nil)
	for _, rec := range ownedFiles {
		m.On("GetUID", rec.Path).Return(toInt(rec.User.Uid), nil)
		m.On("GetGID", rec.Path).Return(toInt(rec.User.Gid), nil)
		m.On("Stat", rec.Path).Return(rec, nil)
	}
	m.On("GetUID", any).Return(0, nil)
	m.On("GetGID", any).Return(0, nil)
	for _, user := range users {
		m.On("LookupID", user.Uid).Return(user, nil)
		m.On("Lookup", user.Username).Return(user, nil)
	}
	for _, group := range groups {
		m.On("LookupGroupID", group.Gid).Return(group, nil)
		m.On("LookupGroup", group.Name).Return(group, nil)
	}
	m.On("Lookup", any).Return(defaultUser, nil)
	m.On("LookupID", any).Return(defaultUser, nil)
	m.On("LookupGroup", any).Return(defaultGroup, nil)
	m.On("LookupGroupID", any).Return(defaultGroup, nil)
	m.On("Stat", any).Return(fakeStat, nil)
	return m
}

func failingMockOS(failOn map[string]error) *MockOS {
	m := &MockOS{}
	m.On("Walk", any, any).Return(failOn["Walk"])
	m.On("Chown", any, any, any).Return(failOn["Chown"])
	m.On("GetUID", any).Return(0, failOn["GetUID"])
	m.On("GetGID", any).Return(0, failOn["GetGID"])
	if err, ok := failOn["Lookup"]; ok {
		m.On("Lookup", any).Return(nil, err)
	} else {
		m.On("Lookup", any).Return(rootUser, nil)
	}
	if err, ok := failOn["LookupGroup"]; ok {
		m.On("LookupGroup", any).Return(nil, err)
	} else {
		m.On("LookupGroup", any).Return(rootGroup, nil)
	}
	if err, ok := failOn["LookupID"]; ok {
		m.On("LookupID", any).Return(nil, err)
	} else {
		m.On("LookupID", any).Return(rootUser, nil)
	}
	if err, ok := failOn["LookupGroupID"]; ok {
		m.On("LookupGroupID", any).Return(nil, err)
	} else {
		m.On("LookupGroupID", any).Return(rootGroup, nil)
	}
	if err, ok := failOn["Stat"]; ok {
		m.On("Stat", any).Return(fakeStat, err)
	} else {
		m.On("Stat", any).Return(fakeStat, nil)
	}
	return m
}

func missingMockOS() *MockOS {
	return failingMockOS(map[string]error{"Stat": os.ErrNotExist})
}

func toInt(s string) int {
	i, _ := strconv.Atoi(s)
	return i
}

func assertIn(t *testing.T, strs []string, s string) {
	for _, str := range strs {
		if s == str {
			return
		}
	}
	t.Errorf("%s is not included in %v", s, strs)
}

func assertNotIn(t *testing.T, strs []string, s string) {
	for _, str := range strs {
		if s == str {
			t.Errorf("%s should not be included in %v", s, strs)
		}
	}
	return
}
