// -*- Mode: Go; indent-tabs-mode: t -*-

/*
 * Copyright (C) Canonical Ltd
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

package ldconfig_test

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

	. "gopkg.in/check.v1"

	"github.com/snapcore/snapd/dirs"
	"github.com/snapcore/snapd/interfaces"
	"github.com/snapcore/snapd/interfaces/ifacetest"
	"github.com/snapcore/snapd/interfaces/ldconfig"
	"github.com/snapcore/snapd/snap"
	"github.com/snapcore/snapd/snap/snaptest"
	"github.com/snapcore/snapd/testutil"
)

func Test(t *testing.T) {
	TestingT(t)
}

type backendSuite struct {
	ifacetest.BackendSuite
	ldconfigCmd *testutil.MockCmd
}

var _ = Suite(&backendSuite{})

func (s *backendSuite) SetUpTest(c *C) {
	s.Backend = &ldconfig.Backend{}
	s.BackendSuite.SetUpTest(c)
	c.Assert(s.Repo.AddBackend(s.Backend), IsNil)
	s.ldconfigCmd = testutil.MockCommand(c, "ldconfig", "")
}

func (s *backendSuite) TearDownTest(c *C) {
	s.ldconfigCmd.Restore()
	s.BackendSuite.TearDownTest(c)
}

func (s *backendSuite) TestName(c *C) {
	c.Check(s.Backend.Name(), Equals, interfaces.SecurityLdconfig)
}

func (s *backendSuite) mockSlot(c *C, yaml string, si *snap.SideInfo, slotName string) (*interfaces.SnapAppSet, *snap.SlotInfo) {
	info := snaptest.MockInfo(c, yaml, si)

	set, err := interfaces.NewSnapAppSet(info, nil)
	c.Assert(err, IsNil)
	err = s.Repo.AddAppSet(set)
	c.Assert(err, IsNil)

	if slotInfo, ok := info.Slots[slotName]; ok {
		return set, slotInfo
	}
	panic(fmt.Sprintf("cannot find slot %q in snap %q", slotName, info.InstanceName()))
}

func (s *backendSuite) mockPlug(c *C, yaml string, si *snap.SideInfo, plugName string) (*interfaces.SnapAppSet, *snap.PlugInfo) {
	info := snaptest.MockInfo(c, yaml, si)

	set, err := interfaces.NewSnapAppSet(info, nil)
	c.Assert(err, IsNil)
	err = s.Repo.AddAppSet(set)
	c.Assert(err, IsNil)

	if plugInfo, ok := info.Plugs[plugName]; ok {
		return set, plugInfo
	}
	panic(fmt.Sprintf("cannot find plug %q in snap %q", plugName, info.InstanceName()))
}

const cudaProvider1 = `name: cuda1
version: 0
type: app
slots:
  cuda-driver-libs:
`
const cudaProvider2 = `name: cuda2
version: 0
type: app
slots:
  cuda-driver-libs:
`

const cudaConsumer = `name: snapd
version: 0
type: snapd
plugs:
  cuda-driver-libs:
apps:
  app:
    plugs: [cuda-driver-libs]
`

func (s *backendSuite) TestInstallingCreatesLdconf(c *C) {
	// Default content for /etc/ld.so.conf.d/
	confDir := filepath.Join(dirs.GlobalRootDir, "etc", "ld.so.conf.d")
	c.Assert(os.MkdirAll(confDir, 0755), IsNil)
	libcConfPath := filepath.Join(confDir, "libc.conf")
	c.Assert(os.WriteFile(libcConfPath, []byte{}, 0644), IsNil)

	// Add callbacks and register the interface
	s.Iface.LdconfigConnectedPlugCallback = func(spec *ldconfig.Specification,
		plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error {
		switch slot.Snap().InstanceName() {
		case "cuda1":
			spec.AddLibDirs([]string{"/dir1/lib1", "/dir1/lib11"})
		case "cuda2":
			spec.AddLibDirs([]string{"/dir2/lib1", "/dir2/lib11"})
		}
		return nil
	}

	s.Iface.InterfaceName = "cuda-driver-libs"
	c.Assert(s.Repo.AddInterface(s.Iface), IsNil)

	// Mock plug/slots
	appSet, plugInfo := s.mockPlug(c, cudaConsumer, nil, "cuda-driver-libs")
	slotAppSet1, slotInfo1 := s.mockSlot(c, cudaProvider1, nil, "cuda-driver-libs")
	slotAppSet2, slotInfo2 := s.mockSlot(c, cudaProvider2, nil, "cuda-driver-libs")

	// Connect them
	connRef1 := interfaces.NewConnRef(plugInfo, slotInfo1)
	_, err := s.Repo.Connect(connRef1, nil, nil, nil, nil, nil)
	c.Assert(err, IsNil)
	connRef2 := interfaces.NewConnRef(plugInfo, slotInfo2)
	_, err = s.Repo.Connect(connRef2, nil, nil, nil, nil, nil)
	c.Assert(err, IsNil)
	// Set-up the backends. The calls for the slot must not have any effect in this case.
	c.Assert(s.Backend.Setup(appSet, interfaces.ConfinementOptions{}, interfaces.SetupContext{Reason: interfaces.SnapSetupReasonOther}, s.Repo, nil), IsNil)
	c.Assert(s.Backend.Setup(slotAppSet1, interfaces.ConfinementOptions{}, interfaces.SetupContext{Reason: interfaces.SnapSetupReasonOther}, s.Repo, nil), IsNil)
	c.Assert(s.Backend.Setup(slotAppSet2, interfaces.ConfinementOptions{}, interfaces.SetupContext{Reason: interfaces.SnapSetupReasonOther}, s.Repo, nil), IsNil)

	confPath := filepath.Join(dirs.GlobalRootDir, "etc", "ld.so.conf.d", "snap.system.conf")
	c.Assert(confPath, testutil.FileEquals, `## This file is automatically generated by snapd

# Directories from cuda1 snap, cuda-driver-libs slot
/dir1/lib1
/dir1/lib11

# Directories from cuda2 snap, cuda-driver-libs slot
/dir2/lib1
/dir2/lib11
`)
	c.Assert(s.ldconfigCmd.Calls(), DeepEquals, [][]string{{"ldconfig"}})
	s.ldconfigCmd.ForgetCalls()

	// Now disconnect the first slot and set-up backends again
	c.Assert(s.Repo.Disconnect(plugInfo.Snap.InstanceName(), plugInfo.Name,
		slotInfo1.Snap.InstanceName(), slotInfo1.Name), IsNil)
	c.Assert(s.Backend.Setup(appSet, interfaces.ConfinementOptions{}, interfaces.SetupContext{Reason: interfaces.SnapSetupReasonOther}, s.Repo, nil), IsNil)
	c.Assert(s.Backend.Setup(slotAppSet1, interfaces.ConfinementOptions{}, interfaces.SetupContext{Reason: interfaces.SnapSetupReasonOther}, s.Repo, nil), IsNil)
	c.Assert(s.Backend.Setup(slotAppSet2, interfaces.ConfinementOptions{}, interfaces.SetupContext{Reason: interfaces.SnapSetupReasonOther}, s.Repo, nil), IsNil)

	c.Assert(confPath, testutil.FileEquals, `## This file is automatically generated by snapd

# Directories from cuda2 snap, cuda-driver-libs slot
/dir2/lib1
/dir2/lib11
`)
	c.Assert(s.ldconfigCmd.Calls(), DeepEquals, [][]string{{"ldconfig"}})
	s.ldconfigCmd.ForgetCalls()

	// libc config has not been touched
	c.Check(libcConfPath, testutil.FilePresent)
}

func (s *backendSuite) TestSandboxFeatures(c *C) {
	c.Assert(s.Backend.SandboxFeatures(), DeepEquals, []string{"mediated-ldconfig"})
}

func (s *backendSuite) TestInstallingLdconfigFileCreatedRemoved(c *C) {
	// Default content for /etc/ld.so.conf.d/
	confDir := filepath.Join(dirs.GlobalRootDir, "etc", "ld.so.conf.d")
	c.Assert(os.MkdirAll(confDir, 0755), IsNil)
	libcConfPath := filepath.Join(confDir, "libc.conf")
	c.Assert(os.WriteFile(libcConfPath, []byte{}, 0644), IsNil)

	// register the interface, no libs added from the callback
	s.Iface.InterfaceName = "cuda-driver-libs"
	c.Assert(s.Repo.AddInterface(s.Iface), IsNil)

	// Mock plug/slots
	appSet, plugInfo := s.mockPlug(c, cudaConsumer, nil, "cuda-driver-libs")
	_, slotInfo1 := s.mockSlot(c, cudaProvider1, nil, "cuda-driver-libs")

	// Connect them
	connRef1 := interfaces.NewConnRef(plugInfo, slotInfo1)
	_, err := s.Repo.Connect(connRef1, nil, nil, nil, nil, nil)
	c.Assert(err, IsNil)
	// Set-up the backends
	c.Assert(s.Backend.Setup(appSet, interfaces.ConfinementOptions{}, interfaces.SetupContext{Reason: interfaces.SnapSetupReasonOther}, s.Repo, nil), IsNil)

	confPath := filepath.Join(dirs.GlobalRootDir, "etc", "ld.so.conf.d", "snap.system.conf")
	c.Assert(confPath, testutil.FileAbsent)
	c.Assert(s.ldconfigCmd.Calls(), IsNil)

	// disconnect and setup, ldconfig not run either
	c.Assert(s.Repo.Disconnect(plugInfo.Snap.InstanceName(), plugInfo.Name,
		slotInfo1.Snap.InstanceName(), slotInfo1.Name), IsNil)
	c.Assert(s.Backend.Setup(appSet, interfaces.ConfinementOptions{}, interfaces.SetupContext{Reason: interfaces.SnapSetupReasonOther}, s.Repo, nil), IsNil)
	c.Assert(confPath, testutil.FileAbsent)
	c.Assert(s.ldconfigCmd.Calls(), IsNil)

	// Add callback that gives now some content
	s.Iface.LdconfigConnectedPlugCallback = func(spec *ldconfig.Specification,
		plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error {
		spec.AddLibDirs([]string{"/dir1/lib1", "/dir1/lib11"})
		return nil
	}

	// Connect again
	_, err = s.Repo.Connect(connRef1, nil, nil, nil, nil, nil)
	c.Assert(err, IsNil)
	// Set-up the backends
	c.Assert(s.Backend.Setup(appSet, interfaces.ConfinementOptions{}, interfaces.SetupContext{Reason: interfaces.SnapSetupReasonOther}, s.Repo, nil), IsNil)

	c.Assert(confPath, testutil.FileEquals, `## This file is automatically generated by snapd

# Directories from cuda1 snap, cuda-driver-libs slot
/dir1/lib1
/dir1/lib11
`)
	c.Assert(s.ldconfigCmd.Calls(), DeepEquals, [][]string{{"ldconfig"}})
	s.ldconfigCmd.ForgetCalls()

	// Set-up again, but no change, no calls to ldconfig
	c.Assert(s.Backend.Setup(appSet, interfaces.ConfinementOptions{}, interfaces.SetupContext{Reason: interfaces.SnapSetupReasonOther}, s.Repo, nil), IsNil)

	c.Assert(confPath, testutil.FileEquals, `## This file is automatically generated by snapd

# Directories from cuda1 snap, cuda-driver-libs slot
/dir1/lib1
/dir1/lib11
`)
	c.Assert(s.ldconfigCmd.Calls(), IsNil)

	// disconnect then Setup now removed the configuration and calls ldconfig
	c.Assert(s.Repo.Disconnect(plugInfo.Snap.InstanceName(), plugInfo.Name,
		slotInfo1.Snap.InstanceName(), slotInfo1.Name), IsNil)
	c.Assert(s.Backend.Setup(appSet, interfaces.ConfinementOptions{}, interfaces.SetupContext{Reason: interfaces.SnapSetupReasonOther}, s.Repo, nil), IsNil)
	c.Assert(confPath, testutil.FileAbsent)
	c.Assert(s.ldconfigCmd.Calls(), DeepEquals, [][]string{{"ldconfig"}})
	s.ldconfigCmd.ForgetCalls()
}
