// 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 (
	"context"
	"errors"
	"os/user"
	"testing"

	"github.com/asteris-llc/converge/helpers/fakerenderer"
	"github.com/asteris-llc/converge/resource"
	"github.com/asteris-llc/converge/resource/file/owner"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// TestInterface ensures that owner.Owner implements the resource.Task interface
func TestInterface(t *testing.T) {
	t.Parallel()
	assert.Implements(t, (*resource.Task)(nil), new(owner.Owner))
}

// TestCheck tests the behavior of Check
func TestCheck(t *testing.T) {
	t.Parallel()

	users := []*user.User{
		fakeUser("1", "1", "user-1"),
		fakeUser("2", "2", "user-2"),
	}

	groups := []*user.Group{
		fakeGroup("1", "group-1"),
		fakeGroup("2", "group-2"),
	}

	ownershipRecords := []ownershipRecord{
		makeOwned("foo", "user-1", "1", "group-1", "1"),
	}

	m := newMockOS(ownershipRecords, users, groups, nil, nil)

	t.Run("when-user-and-group-no-change", func(t *testing.T) {
		t.Parallel()

		o := (&owner.Owner{
			Destination: "foo",
			Username:    "user-1",
			UID:         "1",
			Group:       "group-1",
			GID:         "1",
		}).SetOSProxy(m)
		status, err := o.Check(context.Background(), fakerenderer.New())
		require.NoError(t, err)
		assertIn(t, status.Messages(), "Checking file ownership at: foo")
		m.AssertCalled(t, "GetGID", any)
		m.AssertCalled(t, "GetUID", any)
		assert.False(t, status.HasChanges())
	})

	t.Run("when-missing", func(t *testing.T) {
		t.Parallel()
		m := missingMockOS()
		o := (&owner.Owner{
			Destination: "foo",
			Username:    "user-1",
			UID:         "1",
			Group:       "group-1",
			GID:         "1",
		}).SetOSProxy(m)
		status, err := o.Check(context.Background(), fakerenderer.New())
		require.NoError(t, err)
		assertIn(t, status.Messages(), "foo: does not exist; will re-attempt during apply")
		assert.True(t, status.HasChanges())
		m.AssertNotCalled(t, "GetGID", any)
		m.AssertNotCalled(t, "GetUID", any)
	})

	t.Run("when-user-no-change", func(t *testing.T) {
		t.Parallel()

		o := (&owner.Owner{
			Destination: "foo",
			Username:    "user-1",
			UID:         "1",
		}).SetOSProxy(m)
		status, err := o.Check(context.Background(), fakerenderer.New())
		require.NoError(t, err)
		assertIn(t, status.Messages(), "Checking file ownership at: foo")
		m.AssertCalled(t, "GetGID", any)
		m.AssertCalled(t, "GetUID", any)
		assert.False(t, status.HasChanges())
	})

	t.Run("when-group-no-change", func(t *testing.T) {
		t.Parallel()

		o := (&owner.Owner{
			Destination: "foo",
			Group:       "group-1",
			GID:         "1",
		}).SetOSProxy(m)
		status, err := o.Check(context.Background(), fakerenderer.New())
		require.NoError(t, err)
		assertIn(t, status.Messages(), "Checking file ownership at: foo")
		m.AssertCalled(t, "GetGID", any)
		m.AssertCalled(t, "GetUID", any)
		assert.False(t, status.HasChanges())
	})

	t.Run("when-user-and-group-change", func(t *testing.T) {
		t.Parallel()

		o := (&owner.Owner{
			Destination: "foo",
			Username:    "user-2",
			UID:         "2",
			Group:       "group-2",
			GID:         "2",
		}).SetOSProxy(m)
		status, err := o.Check(context.Background(), fakerenderer.New())
		require.NoError(t, err)
		assertIn(t, status.Messages(), "Checking file ownership at: foo")
		m.AssertCalled(t, "GetGID", any)
		m.AssertCalled(t, "GetUID", any)
		assert.True(t, status.HasChanges())
		resStat, ok := status.(*resource.Status)
		require.True(t, ok)
		fooDiffs, ok := resStat.Differences["foo"]
		require.True(t, ok)
		diff, ok := fooDiffs.(*owner.OwnershipDiff)
		require.True(t, ok)
		require.NotNil(t, diff.UIDs)
		require.NotNil(t, diff.GIDs)
		assert.Equal(t, [2]int{1, 2}, *(diff.UIDs))
		assert.Equal(t, [2]int{1, 2}, *(diff.GIDs))
	})

	t.Run("when-user-change-and-group-no-change", func(t *testing.T) {
		t.Parallel()

		o := (&owner.Owner{
			Destination: "foo",
			Username:    "user-2",
			UID:         "2",
			Group:       "group-1",
			GID:         "1",
		}).SetOSProxy(m)
		status, err := o.Check(context.Background(), fakerenderer.New())
		require.NoError(t, err)
		m.AssertCalled(t, "GetGID", any)
		m.AssertCalled(t, "GetUID", any)
		assert.True(t, status.HasChanges())
		resStat, ok := status.(*resource.Status)
		require.True(t, ok)
		fooDiffs, ok := resStat.Differences["foo"]
		require.True(t, ok)
		diff, ok := fooDiffs.(*owner.OwnershipDiff)
		require.True(t, ok)
		require.NotNil(t, diff.UIDs)
		require.Nil(t, diff.GIDs)
		assert.Equal(t, [2]int{1, 2}, *(diff.UIDs))
	})

	t.Run("when-user-change-and-group-unspecified", func(t *testing.T) {
		t.Parallel()

		o := (&owner.Owner{
			Destination: "foo",
			Username:    "user-2",
			UID:         "2",
			Group:       "",
			GID:         "",
		}).SetOSProxy(m)
		status, err := o.Check(context.Background(), fakerenderer.New())
		require.NoError(t, err)
		m.AssertCalled(t, "GetGID", any)
		m.AssertCalled(t, "GetUID", any)
		assert.True(t, status.HasChanges())
		resStat, ok := status.(*resource.Status)
		require.True(t, ok)
		fooDiffs, ok := resStat.Differences["foo"]
		require.True(t, ok)
		diff, ok := fooDiffs.(*owner.OwnershipDiff)
		require.True(t, ok)
		require.NotNil(t, diff.UIDs)
		require.Nil(t, diff.GIDs)
		assert.Equal(t, [2]int{1, 2}, *(diff.UIDs))
	})

	t.Run("when-group-change-and-user-no-change", func(t *testing.T) {
		t.Parallel()

		o := (&owner.Owner{
			Destination: "foo",
			Username:    "user-1",
			UID:         "1",
			Group:       "group-2",
			GID:         "2",
		}).SetOSProxy(m)
		status, err := o.Check(context.Background(), fakerenderer.New())
		require.NoError(t, err)
		m.AssertCalled(t, "GetGID", any)
		m.AssertCalled(t, "GetUID", any)
		assert.True(t, status.HasChanges())
		resStat, ok := status.(*resource.Status)
		require.True(t, ok)
		fooDiffs, ok := resStat.Differences["foo"]
		require.True(t, ok)
		diff, ok := fooDiffs.(*owner.OwnershipDiff)
		require.True(t, ok)
		require.NotNil(t, diff.GIDs)
		require.Nil(t, diff.UIDs)
		assert.Equal(t, [2]int{1, 2}, *(diff.GIDs))
	})

	t.Run("when-group-change-and-user-unspecified", func(t *testing.T) {
		t.Parallel()

		o := (&owner.Owner{
			Destination: "foo",
			Username:    "",
			UID:         "",
			Group:       "group-2",
			GID:         "2",
		}).SetOSProxy(m)
		status, err := o.Check(context.Background(), fakerenderer.New())
		require.NoError(t, err)
		m.AssertCalled(t, "GetGID", any)
		m.AssertCalled(t, "GetUID", any)
		assert.True(t, status.HasChanges())
		resStat, ok := status.(*resource.Status)
		require.True(t, ok)
		fooDiffs, ok := resStat.Differences["foo"]
		require.True(t, ok)
		diff, ok := fooDiffs.(*owner.OwnershipDiff)
		require.True(t, ok)
		require.NotNil(t, diff.GIDs)
		require.Nil(t, diff.UIDs)
		assert.Equal(t, [2]int{1, 2}, *(diff.GIDs))
	})

	t.Run("when-recursive", func(t *testing.T) {
		t.Parallel()
		t.Run("when-all-change", func(t *testing.T) {
			t.Parallel()
			ownershipRecords := []ownershipRecord{
				makeOwnedFull("root", "user-1", "1", "group-1", "1", true, 0, 0755),
				makeOwnedFull("root/dir1", "user-1", "1", "group-1", "1", true, 0, 0755),
				makeOwnedFull("root/dir1/a", "user-1", "1", "group-1", "1", false, 0, 0755),
				makeOwnedFull("root/dir2", "user-1", "1", "group-1", "1", true, 0, 0755),
				makeOwnedFull("root/dir2/a", "user-1", "1", "group-1", "1", false, 0, 0755),
				makeOwnedFull("root/dir2/b", "user-1", "1", "group-1", "1", false, 0, 0755),
				makeOwnedFull("root/file3", "user-1", "1", "group-1", "1", false, 0, 0755),
			}
			m := newMockOS(ownershipRecords, users, groups, nil, nil)
			o := (&owner.Owner{
				Destination: "root",
				Username:    "user-2",
				UID:         "2",
				Group:       "group-2",
				GID:         "2",
				Recursive:   true,
			}).SetOSProxy(m)
			status, err := o.Check(context.Background(), fakerenderer.New())
			require.NoError(t, err)
			m.AssertCalled(t, "Walk", "root", any)
			m.AssertNumberOfCalls(t, "Walk", 1)
			for _, rec := range ownershipRecords {
				m.AssertCalled(t, "GetGID", rec.Path)
				m.AssertCalled(t, "GetUID", rec.Path)
			}
			resStatus, ok := status.(*resource.Status)
			require.True(t, ok)
			diffs := resStatus.Differences
			assert.Equal(t, len(diffs), len(ownershipRecords))
		})
		t.Run("when-some-change", func(t *testing.T) {
			t.Parallel()
			ownershipRecords := []ownershipRecord{
				makeOwnedFull("root", "user-1", "1", "group-1", "1", true, 0, 0755),
				makeOwnedFull("root/dir1", "user-1", "1", "group-1", "1", true, 0, 0755),
				makeOwnedFull("root/dir1/a", "user-1", "1", "group-1", "1", false, 0, 0755),

				makeOwnedFull("root/file3", "user-1", "1", "group-1", "1", false, 0, 0755),
			}
			changingRecords := []ownershipRecord{
				makeOwnedFull("root/dir2", "user-1", "1", "group-2", "2", true, 0, 0755),
				makeOwnedFull("root/dir2/a", "user-1", "1", "group-2", "2", false, 0, 0755),
				makeOwnedFull("root/dir2/b", "user-1", "1", "group-2", "2", false, 0, 0755),
			}
			allRecords := append(ownershipRecords, changingRecords...)
			m := newMockOS(allRecords, users, groups, nil, nil)
			o := (&owner.Owner{
				Destination: "root",
				Username:    "user-1",
				UID:         "1",
				Group:       "group-1",
				GID:         "1",
				Recursive:   true,
			}).SetOSProxy(m)
			status, err := o.Check(context.Background(), fakerenderer.New())
			require.NoError(t, err)
			m.AssertCalled(t, "Walk", "root", any)
			m.AssertNumberOfCalls(t, "Walk", 1)
			for _, rec := range allRecords {
				m.AssertCalled(t, "GetGID", rec.Path)
				m.AssertCalled(t, "GetUID", rec.Path)
			}
			resStatus, ok := status.(*resource.Status)
			require.True(t, ok)
			diffs := resStatus.Differences
			for _, shouldChange := range changingRecords {
				_, ok := diffs[shouldChange.Path]
				assert.True(t, ok)
			}
			assert.Equal(t, len(diffs), len(changingRecords))
		})
	})
	t.Run("verbose-flag", func(t *testing.T) {
		t.Parallel()
		t.Run("when-verbose-true-single-file", func(t *testing.T) {
			t.Parallel()
			o := (&owner.Owner{
				Destination: "foo",
				Username:    "user-2",
				UID:         "2",
				Recursive:   true,
				HideDetails: false,
			}).SetOSProxy(m)
			status, err := o.Check(context.Background(), fakerenderer.New())
			require.NoError(t, err)
			assertIn(t, status.Messages(), "Checking file ownership at: foo")
			assertIn(t, status.Messages(), "Recursive: True")
			assert.Equal(t, 1, len(status.Diffs()))
		})
		t.Run("when-verbose-false-single-file", func(t *testing.T) {
			t.Parallel()
			o := (&owner.Owner{
				Destination: "foo",
				Username:    "user-2",
				UID:         "2",
				Recursive:   true,
				HideDetails: true,
			}).SetOSProxy(m)
			status, err := o.Check(context.Background(), fakerenderer.New())
			assertIn(t, status.Messages(), "Checking file ownership at: foo")
			assertIn(t, status.Messages(), "Recursive: True")
			require.NoError(t, err)
			assert.Equal(t, 1, len(status.Diffs()))
		})
		t.Run("when-verbose-true-many-files", func(t *testing.T) {
			t.Parallel()
			ownershipRecords := []ownershipRecord{
				makeOwned("foo1", "user-1", "1", "group-1", "1"),
				makeOwned("foo2", "user-1", "1", "group-1", "1"),
				makeOwned("foo3", "user-1", "1", "group-1", "1"),
				makeOwned("foo4", "user-1", "1", "group-1", "1"),
			}
			m := newMockOS(ownershipRecords, users, groups, nil, nil)
			o := (&owner.Owner{
				Destination: "foo",
				Username:    "user-2",
				UID:         "2",
				Recursive:   true,
				HideDetails: false,
			}).SetOSProxy(m)
			status, err := o.Check(context.Background(), fakerenderer.New())
			require.NoError(t, err)
			assertIn(t, status.Messages(), "Checking file ownership at: foo")
			assertIn(t, status.Messages(), "Recursive: True")
			assertNotIn(t, status.Messages(), "reporting abridged changes; add \"verbose = true\" to see all changes")
			assert.Equal(t, len(ownershipRecords), len(status.Diffs()))
		})
		t.Run("when-verbose-false-many-files", func(t *testing.T) {
			t.Parallel()
			ownershipRecords := []ownershipRecord{
				makeOwned("foo1", "user-1", "1", "group-1", "1"),
				makeOwned("foo2", "user-1", "1", "group-1", "1"),
				makeOwned("foo3", "user-1", "1", "group-1", "1"),
				makeOwned("foo4", "user-1", "1", "group-1", "1"),
			}

			m := newMockOS(ownershipRecords, users, groups, nil, nil)
			o := (&owner.Owner{
				Destination: "foo",
				Username:    "user-2",
				UID:         "2",
				Recursive:   true,
				HideDetails: true,
			}).SetOSProxy(m)
			status, err := o.Check(context.Background(), fakerenderer.New())
			assertIn(t, status.Messages(), "Recursive: True")
			assertIn(t, status.Messages(), "Checking file ownership at: foo")
			assertIn(t, status.Messages(), "reporting abridged changes; add \"verbose = true\" to see all changes")
			require.NoError(t, err)
			assert.Equal(t, 1, len(status.Diffs()))
		})
		t.Run("when-verbose-false-not-recursive", func(t *testing.T) {
			t.Parallel()
			o := (&owner.Owner{
				Destination: "foo",
				Username:    "user-2",
				UID:         "2",
				HideDetails: true,
			}).SetOSProxy(m)
			status, err := o.Check(context.Background(), fakerenderer.New())
			assertIn(t, status.Messages(), "Checking file ownership at: foo")
			assertNotIn(t, status.Messages(), "Recursive: True")
			assertNotIn(t, status.Messages(), "reporting abridged changes; add \"verbose = true\" to see all changes")
			require.NoError(t, err)
			assert.Equal(t, 1, len(status.Diffs()))
		})
		t.Run("when-verbose-false-no-changes", func(t *testing.T) {
			t.Parallel()
			o := (&owner.Owner{
				Destination: "foo",
				Username:    "user-1",
				UID:         "1",
				HideDetails: true,
			}).SetOSProxy(m)
			status, err := o.Check(context.Background(), fakerenderer.New())
			require.NoError(t, err)
			assertIn(t, status.Messages(), "Checking file ownership at: foo")
			assertNotIn(t, status.Messages(), "Recursive: True")
			assertNotIn(t, status.Messages(), "reporting abridged changes; add \"verbose = true\" to see all changes")
			assert.False(t, status.HasChanges())
		})
	})
}

// TestApply tests the behavior of Apply
func TestApply(t *testing.T) {
	t.Parallel()
	t.Run("single-diff", func(t *testing.T) {
		users := []*user.User{fakeUser("1", "1", "user-1"), fakeUser("2", "2", "user-2")}
		groups := []*user.Group{fakeGroup("1", "group-1"), fakeGroup("2", "group-2")}
		ownershipRecords := []ownershipRecord{makeOwned("foo", "user-1", "1", "group-1", "1")}
		m := newMockOS(ownershipRecords, users, groups, nil, nil)
		o := (&owner.Owner{
			Destination: "foo",
			Username:    "user-2",
			UID:         "2",
			Group:       "group-2",
			GID:         "2",
		}).SetOSProxy(m)
		_, err := o.Check(context.Background(), fakerenderer.New())
		require.NoError(t, err)
		status, err := o.Apply(context.Background())
		require.NoError(t, err)
		assertIn(t, status.Messages(), "Updating permissions on file(s) at: foo")
		assertNotIn(t, status.Messages(), "Recursive: True")
		assertNotIn(t, status.Messages(), "reporting abridged changes; add \"verbose = true\" to see all changes")
		m.AssertCalled(t, "Chown", "foo", 2, 2)
	})
	t.Run("multiple-diff", func(t *testing.T) {
		users := []*user.User{fakeUser("1", "1", "user-1"), fakeUser("2", "2", "user-2")}
		groups := []*user.Group{fakeGroup("1", "group-1"), fakeGroup("2", "group-2")}
		ownershipRecords := []ownershipRecord{makeOwned("foo", "user-1", "1", "group-1", "1"), makeOwned("bar", "user-1", "1", "group-1", "1")}
		m := newMockOS(ownershipRecords, users, groups, nil, nil)
		o := (&owner.Owner{
			Destination: "foo",
			Username:    "user-2",
			Recursive:   true,
			UID:         "2",
			Group:       "group-2",
			GID:         "2",
		}).SetOSProxy(m)
		_, err := o.Check(context.Background(), fakerenderer.New())
		require.NoError(t, err)
		status, err := o.Apply(context.Background())
		require.NoError(t, err)
		assertIn(t, status.Messages(), "Updating permissions on file(s) at: foo")
		assertIn(t, status.Messages(), "Recursive: True")
		assertNotIn(t, status.Messages(), "reporting abridged changes; add \"verbose = true\" to see all changes")
		m.AssertCalled(t, "Chown", "foo", 2, 2)
		m.AssertCalled(t, "Chown", "bar", 2, 2)
		m.AssertNumberOfCalls(t, "Chown", 2)
	})
	t.Run("when-missing", func(t *testing.T) {
		t.Parallel()
		users := []*user.User{fakeUser("1", "1", "user-1"), fakeUser("2", "2", "user-2")}
		groups := []*user.Group{fakeGroup("1", "group-1"), fakeGroup("2", "group-2")}
		ownershipRecords := []ownershipRecord{makeOwned("foo", "user-1", "1", "group-1", "1")}
		m := newMockOS(ownershipRecords, users, groups, nil, nil)
		missing := missingMockOS()
		o := (&owner.Owner{
			Destination: "foo",
			Username:    "user-1",
			UID:         "1",
			Group:       "group-1",
			GID:         "1",
		}).SetOSProxy(missing)
		_, err := o.Check(context.Background(), fakerenderer.New())
		require.NoError(t, err)
		o.SetOSProxy(m)
		_, err = o.Apply(context.Background())
		m.AssertCalled(t, "GetGID", any)
		m.AssertCalled(t, "GetUID", any)
	})
	t.Run("when-missing-still-missing", func(t *testing.T) {
		t.Parallel()
		missing := missingMockOS()
		o := (&owner.Owner{
			Destination: "foo",
			Username:    "user-1",
			UID:         "1",
			Group:       "group-1",
			GID:         "1",
		}).SetOSProxy(missing)
		_, err := o.Check(context.Background(), fakerenderer.New())
		require.NoError(t, err)
		_, err = o.Apply(context.Background())
		assert.Equal(t, errors.New("cannot change ownership of non-existent file: foo"), err)
	})
}
