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

/*
 * Copyright (C) 2016-2024 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 devicestate_test

import (
	"context"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"testing"
	"time"

	. "gopkg.in/check.v1"
	"gopkg.in/tomb.v2"

	"github.com/snapcore/snapd/asserts"
	"github.com/snapcore/snapd/asserts/assertstest"
	"github.com/snapcore/snapd/asserts/sysdb"
	"github.com/snapcore/snapd/boot"
	"github.com/snapcore/snapd/bootloader"
	"github.com/snapcore/snapd/bootloader/bootloadertest"
	"github.com/snapcore/snapd/dirs"
	"github.com/snapcore/snapd/gadget"
	"github.com/snapcore/snapd/interfaces"
	"github.com/snapcore/snapd/interfaces/builtin"
	"github.com/snapcore/snapd/kernel/fde"
	"github.com/snapcore/snapd/logger"
	"github.com/snapcore/snapd/osutil"
	"github.com/snapcore/snapd/osutil/keyboard"
	"github.com/snapcore/snapd/overlord"
	"github.com/snapcore/snapd/overlord/assertstate"
	"github.com/snapcore/snapd/overlord/assertstate/assertstatetest"
	"github.com/snapcore/snapd/overlord/auth"
	"github.com/snapcore/snapd/overlord/configstate/config"
	"github.com/snapcore/snapd/overlord/devicestate"
	"github.com/snapcore/snapd/overlord/devicestate/devicestatetest"
	"github.com/snapcore/snapd/overlord/hookstate"
	"github.com/snapcore/snapd/overlord/ifacestate/ifacerepo"
	"github.com/snapcore/snapd/overlord/restart"
	"github.com/snapcore/snapd/overlord/snapstate"
	"github.com/snapcore/snapd/overlord/snapstate/snapstatetest"
	"github.com/snapcore/snapd/overlord/state"
	"github.com/snapcore/snapd/overlord/storecontext"
	"github.com/snapcore/snapd/release"
	"github.com/snapcore/snapd/secboot"
	"github.com/snapcore/snapd/secboot/keys"
	"github.com/snapcore/snapd/seed"
	"github.com/snapcore/snapd/seed/seedtest"
	"github.com/snapcore/snapd/seed/seedwriter"
	"github.com/snapcore/snapd/snap"
	"github.com/snapcore/snapd/snap/snaptest"
	"github.com/snapcore/snapd/snapdenv"
	"github.com/snapcore/snapd/store/storetest"
	"github.com/snapcore/snapd/sysconfig"
	"github.com/snapcore/snapd/testutil"
	"github.com/snapcore/snapd/timeutil"
	"github.com/snapcore/snapd/timings"
)

var (
	settleTimeout = testutil.HostScaledTimeout(30 * time.Second)
)

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

type deviceMgrBaseSuite struct {
	testutil.BaseTest

	o       *overlord.Overlord
	state   *state.State
	se      *overlord.StateEngine
	hookMgr *hookstate.HookManager
	mgr     *devicestate.DeviceManager
	db      *asserts.Database

	bootloader *bootloadertest.MockBootloader

	storeSigning *assertstest.StoreStack
	brands       *assertstest.SigningAccounts

	ancillary []asserts.Assertion

	restartRequests []restart.RestartType
	restartObserve  func()

	newFakeStore func(storecontext.DeviceBackend) snapstate.StoreService

	// saved so that if a derived suite wants to undo the cloud-init mocking to
	// test the actual functions, it can just call this in it's SetUpTest, see
	// devicestate_cloudinit_test.go for details
	restoreCloudInitStatusRestore func()

	restoreProcessAutoImportAssertion func()
}

// mockRestartAndSettle expects the state to be locked
func (s *deviceMgrBaseSuite) mockRestartAndSettle(c *C, st *state.State, chg *state.Change) {
	restart.MockPending(st, restart.RestartUnset)
	restart.MockAfterRestartForChange(chg)

	st.Unlock()
	defer st.Lock()
	err := s.o.Settle(settleTimeout)
	c.Check(err, IsNil)
}

type deviceMgrSuite struct {
	deviceMgrBaseSuite
}

var _ = Suite(&deviceMgrSuite{})

type fakeStore struct {
	storetest.Store

	state *state.State
	db    asserts.RODatabase
}

func (sto *fakeStore) pokeStateLock() {
	// the store should be called without the state lock held. Try
	// to acquire it.
	sto.state.Lock()
	sto.state.Unlock()
}

func (sto *fakeStore) Assertion(assertType *asserts.AssertionType, key []string, _ *auth.UserState) (asserts.Assertion, error) {
	sto.pokeStateLock()
	ref := &asserts.Ref{Type: assertType, PrimaryKey: key}
	return ref.Resolve(sto.db.Find)
}

func (sto *fakeStore) SeqFormingAssertion(assertType *asserts.AssertionType, key []string, sequence int, user *auth.UserState) (asserts.Assertion, error) {
	sto.pokeStateLock()

	ref := &asserts.AtSequence{
		Type:        assertType,
		SequenceKey: key,
		Sequence:    sequence,
		Revision:    asserts.RevisionNotKnown,
		Pinned:      sequence > 0,
	}

	if sequence <= 0 {
		hdrs, err := asserts.HeadersFromSequenceKey(ref.Type, ref.SequenceKey)
		if err != nil {
			return nil, err
		}
		return sto.db.FindSequence(ref.Type, hdrs, -1, ref.Type.MaxSupportedFormat())
	}

	return ref.Resolve(sto.db.Find)
}

var (
	brandPrivKey, _  = assertstest.GenerateKey(752)
	brandPrivKey2, _ = assertstest.GenerateKey(752)
	brandPrivKey3, _ = assertstest.GenerateKey(752)
)

func (s *deviceMgrBaseSuite) setupBaseTest(c *C, classic bool) {
	s.BaseTest.SetUpTest(c)

	s.AddCleanup(release.MockOnClassic(classic))

	dirs.SetRootDir(c.MkDir())
	s.AddCleanup(func() { dirs.SetRootDir("") })

	err := os.MkdirAll(dirs.SnapRunDir, 0755)
	c.Assert(err, IsNil)
	err = os.MkdirAll(dirs.SnapdStateDir(dirs.GlobalRootDir), 0755)
	c.Assert(err, IsNil)

	s.AddCleanup(osutil.MockMountInfo(``))

	s.restartRequests = nil

	s.AddCleanup(snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {}))

	s.bootloader = bootloadertest.Mock("mock", c.MkDir())
	bootloader.Force(s.bootloader)
	s.AddCleanup(func() { bootloader.Force(nil) })

	s.storeSigning = assertstest.NewStoreStack("canonical", nil)
	s.restartObserve = nil
	s.o = overlord.Mock()
	s.state = s.o.State()
	s.state.Lock()
	_, err = restart.Manager(s.state, "boot-id-0", snapstatetest.MockRestartHandler(func(req restart.RestartType) {
		s.restartRequests = append(s.restartRequests, req)
		if s.restartObserve != nil {
			s.restartObserve()
		}
	}))
	s.state.Unlock()
	c.Assert(err, IsNil)
	s.se = s.o.StateEngine()

	s.AddCleanup(sysdb.MockGenericClassicModel(s.storeSigning.GenericClassicModel))

	s.brands = assertstest.NewSigningAccounts(s.storeSigning)
	s.brands.Register("my-brand", brandPrivKey, map[string]any{
		"display-name": "fancy model publisher",
		"validation":   "certified",
	})
	s.brands.Register("rereg-brand", brandPrivKey2, nil)

	db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{
		Backstore:       asserts.NewMemoryBackstore(),
		Trusted:         s.storeSigning.Trusted,
		OtherPredefined: s.storeSigning.Generic,
	})
	c.Assert(err, IsNil)

	s.state.Lock()
	assertstate.ReplaceDB(s.state, db)
	s.state.Unlock()
	s.AddCleanup(func() {
		s.state.Lock()
		assertstate.ReplaceDB(s.state, nil)
		s.state.Unlock()
	})

	err = db.Add(s.storeSigning.StoreAccountKey(""))
	c.Assert(err, IsNil)

	hookMgr, err := hookstate.Manager(s.state, s.o.TaskRunner())
	c.Assert(err, IsNil)

	devicestate.EarlyConfig = func(*state.State, func() (sysconfig.Device, *gadget.Info, error)) error {
		return nil
	}
	s.AddCleanup(func() { devicestate.EarlyConfig = nil })

	mgr, err := devicestate.Manager(s.state, hookMgr, s.o.TaskRunner(), s.newStore)
	c.Assert(err, IsNil)

	s.db = db
	s.hookMgr = hookMgr
	s.o.AddManager(s.hookMgr)
	s.mgr = mgr
	s.o.AddManager(s.mgr)
	s.o.AddManager(s.o.TaskRunner())

	// For triggering errors
	erroringHandler := func(task *state.Task, _ *tomb.Tomb) error {
		return errors.New("error out")
	}
	s.o.TaskRunner().AddHandler("error-trigger", erroringHandler, nil)

	c.Assert(s.o.StartUp(), IsNil)

	s.state.Lock()
	snapstate.ReplaceStore(s.state, &fakeStore{
		state: s.state,
		db:    s.storeSigning,
	})
	s.state.Unlock()

	s.restoreCloudInitStatusRestore = devicestate.MockCloudInitStatus(func() (sysconfig.CloudInitState, error) {
		return sysconfig.CloudInitRestrictedBySnapd, nil
	})
	s.AddCleanup(s.restoreCloudInitStatusRestore)

	s.AddCleanup(func() { s.ancillary = nil })
	s.AddCleanup(func() { s.newFakeStore = nil })

	s.AddCleanup(devicestate.MockTimeutilIsNTPSynchronized(func() (bool, error) {
		return true, nil
	}))
	s.AddCleanup(devicestate.MockSecbootMarkSuccessful(func() error {
		return nil
	}))

	s.restoreProcessAutoImportAssertion = devicestate.MockProcessAutoImportAssertion(func(*state.State, seed.Seed, asserts.RODatabase, func(batch *asserts.Batch) error) error {
		return nil
	})
	s.AddCleanup(s.restoreProcessAutoImportAssertion)

	s.AddCleanup(snapstatetest.MockProcessDelayedSecurityBackendEffects(func(st *state.State, lanes []int, applyInLane int) *state.TaskSet {
		// no reason for devicestate to set up tasks for delayed effects
		panic("unexpected call")
	}))
}

func (s *deviceMgrBaseSuite) newStore(devBE storecontext.DeviceBackend) snapstate.StoreService {
	return s.newFakeStore(devBE)
}

func (s *deviceMgrBaseSuite) settle(c *C) {
	err := s.o.Settle(settleTimeout)
	c.Assert(err, IsNil)
}

// seeding avoids triggering a real full seeding, it simulates having it in process instead
func (s *deviceMgrBaseSuite) seeding() {
	chg := s.state.NewChange("seed", "Seed system")
	chg.SetStatus(state.DoingStatus)
	s.state.Set("seeded", false)
}

func (s *deviceMgrBaseSuite) makeModelAssertionInState(c *C, brandID, model string, extras map[string]any) *asserts.Model {
	modelAs := s.brands.Model(brandID, model, extras)

	s.setupBrands()
	assertstatetest.AddMany(s.state, modelAs)
	return modelAs
}

func (s *deviceMgrBaseSuite) setPCModelInState(c *C) {
	s.state.Lock()
	defer s.state.Unlock()
	s.makeModelAssertionInState(c, "canonical", "pc", map[string]any{
		"architecture": "amd64",
		"kernel":       "pc-kernel",
		"gadget":       "pc",
	})

	devicestatetest.SetDevice(s.state, &auth.DeviceState{
		Brand:  "canonical",
		Model:  "pc",
		Serial: "serialserialserial",
	})
}

func (s *deviceMgrBaseSuite) setUC20PCModelInState(c *C) {
	s.state.Lock()
	defer s.state.Unlock()
	s.makeModelAssertionInState(c, "canonical", "pc-20", map[string]any{
		"architecture": "amd64",
		// UC20
		"grade": "dangerous",
		"base":  "core20",
		"snaps": []any{
			map[string]any{
				"name":            "pc-kernel",
				"id":              snaptest.AssertedSnapID("pc-kernel"),
				"type":            "kernel",
				"default-channel": "20",
			},
			map[string]any{
				"name":            "pc",
				"id":              snaptest.AssertedSnapID("pc"),
				"type":            "gadget",
				"default-channel": "20",
			},
		},
	})

	devicestatetest.SetDevice(s.state, &auth.DeviceState{
		Brand:  "canonical",
		Model:  "pc-20",
		Serial: "serialserialserial",
	})
}

func (s *deviceMgrSuite) setHybridModelInState(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	s.makeModelAssertionInState(c, "canonical", "pc", map[string]any{
		"classic":      "true",
		"distribution": "ubuntu",
		"architecture": "amd64",
		"base":         "core24",
		"snaps": []any{
			map[string]any{
				"name": "pc-kernel",
				"id":   snaptest.AssertedSnapID("pc-kernel"),
				"type": "kernel",
			},
			map[string]any{
				"name": "pc",
				"id":   snaptest.AssertedSnapID("pc"),
				"type": "gadget",
			},
		},
	})

	devicestatetest.SetDevice(s.state, &auth.DeviceState{
		Brand:  "canonical",
		Model:  "pc",
		Serial: "serialserialserial",
	})
}

func (s *deviceMgrBaseSuite) setupBrands() {
	assertstatetest.AddMany(s.state, s.brands.AccountsAndKeys("my-brand")...)
	otherAcct := assertstest.NewAccount(s.storeSigning, "other-brand", map[string]any{
		"account-id": "other-brand",
	}, "")
	assertstatetest.AddMany(s.state, otherAcct)
}

func (s *deviceMgrBaseSuite) setupSnapDeclForNameAndID(c *C, name, snapID, publisherID string) {
	snapDecl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, map[string]any{
		"series":       "16",
		"snap-name":    name,
		"snap-id":      snapID,
		"publisher-id": publisherID,
		"timestamp":    time.Now().UTC().Format(time.RFC3339),
	}, nil, "")
	c.Assert(err, IsNil)
	assertstatetest.AddMany(s.state, snapDecl)
}

func (s *deviceMgrBaseSuite) setupSnapResourcePair(c *C, comp, snapID, publisherID string, resRev, snapRev snap.Revision) {
	assertion, err := s.storeSigning.Sign(asserts.SnapResourcePairType, map[string]any{
		"snap-id":           snapID,
		"resource-name":     comp,
		"resource-revision": strconv.Itoa(resRev.N),
		"snap-revision":     strconv.Itoa(snapRev.N),
		"developer-id":      publisherID,
		"timestamp":         time.Now().UTC().Format(time.RFC3339),
	}, nil, "")
	c.Assert(err, IsNil)
	assertstatetest.AddMany(s.state, assertion)
}

func (s *deviceMgrBaseSuite) setupSnapResourceRevision(c *C, file string, comp, snapID, publisherID string, rev snap.Revision) {
	sha, size, err := asserts.SnapFileSHA3_384(file)
	c.Assert(err, IsNil)

	assertion, err := s.storeSigning.Sign(asserts.SnapResourceRevisionType, map[string]any{
		"snap-id":           snapID,
		"resource-name":     comp,
		"resource-sha3-384": sha,
		"resource-size":     fmt.Sprint(size),
		"resource-revision": strconv.Itoa(rev.N),
		"developer-id":      publisherID,
		"timestamp":         time.Now().UTC().Format(time.RFC3339),
	}, nil, "")
	c.Assert(err, IsNil)
	assertstatetest.AddMany(s.state, assertion)
}

func (s *deviceMgrBaseSuite) setupSnapDecl(c *C, info *snap.Info, publisherID string) {
	s.setupSnapDeclForNameAndID(c, info.SnapName(), info.SnapID, publisherID)
}

func (s *deviceMgrBaseSuite) setupSnapRevisionForFileAndID(c *C, file, snapID, publisherID string, revision snap.Revision) {
	sha3_384, size, err := asserts.SnapFileSHA3_384(file)
	c.Assert(err, IsNil)

	snapRev, err := s.storeSigning.Sign(asserts.SnapRevisionType, map[string]any{
		"snap-sha3-384": sha3_384,
		"snap-size":     fmt.Sprintf("%d", size),
		"snap-id":       snapID,
		"developer-id":  publisherID,
		"snap-revision": revision.String(),
		"timestamp":     time.Now().UTC().Format(time.RFC3339),
	}, nil, "")
	c.Assert(err, IsNil)
	assertstatetest.AddMany(s.state, snapRev)
}

func (s *deviceMgrBaseSuite) setupSnapRevision(c *C, info *snap.Info, publisherID string, revision snap.Revision) {
	s.setupSnapRevisionForFileAndID(c, info.MountFile(), info.SnapID, publisherID, revision)
}

func makeSerialAssertionInState(c *C, brands *assertstest.SigningAccounts, st *state.State, brandID, model, serialN string) *asserts.Serial {
	encDevKey, err := asserts.EncodePublicKey(devKey.PublicKey())
	c.Assert(err, IsNil)
	serial, err := brands.Signing(brandID).Sign(asserts.SerialType, map[string]any{
		"brand-id":            brandID,
		"model":               model,
		"serial":              serialN,
		"device-key":          string(encDevKey),
		"device-key-sha3-384": devKey.PublicKey().ID(),
		"timestamp":           time.Now().Format(time.RFC3339),
	}, nil, "")
	c.Assert(err, IsNil)
	err = assertstate.Add(st, serial)
	c.Assert(err, IsNil)
	return serial.(*asserts.Serial)
}

func (s *deviceMgrBaseSuite) makeSerialAssertionInState(c *C, brandID, model, serialN string) *asserts.Serial {
	return makeSerialAssertionInState(c, s.brands, s.state, brandID, model, serialN)
}

func (s *deviceMgrBaseSuite) addKeyToManagerInState(c *C) {
	device, err := devicestatetest.Device(s.state)
	c.Assert(err, IsNil)

	err = devicestate.KeypairManager(s.mgr).Put(devKey)
	c.Assert(err, IsNil)

	err = devicestatetest.SetDevice(s.state, &auth.DeviceState{
		Brand:  device.Brand,
		Model:  device.Model,
		Serial: device.Serial,
		KeyID:  devKey.PublicKey().ID(),
	})
	c.Assert(err, IsNil)
}

func (s *deviceMgrBaseSuite) DeviceManager() *devicestate.DeviceManager {
	return s.mgr
}

func (s *deviceMgrSuite) SetUpTest(c *C) {
	classic := false
	s.setupBaseTest(c, classic)
}

func (s *deviceMgrSuite) TestDeviceManagerSetTimeOnce(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	// set first time
	now := time.Now()
	err := devicestate.SetTimeOnce(s.mgr, "key-name", now)
	c.Assert(err, IsNil)

	later := now.Add(1 * time.Minute)
	// setting again doesn't change value
	err = devicestate.SetTimeOnce(s.mgr, "key-name", later)
	c.Assert(err, IsNil)

	var t time.Time
	s.state.Get("key-name", &t)

	c.Assert(t.Equal(now), Equals, true)
}

func (s *deviceMgrSuite) TestDeviceManagerEnsureSeededAlreadySeeded(c *C) {
	s.state.Lock()
	s.state.Set("seeded", true)
	s.state.Unlock()

	called := false
	restore := devicestate.MockPopulateStateFromSeed(s.mgr, func(string, string, timings.Measurer) ([]*state.TaskSet, error) {
		called = true
		return nil, nil
	})
	defer restore()

	err := devicestate.EnsureSeeded(s.mgr)
	c.Assert(err, IsNil)
	c.Assert(called, Equals, false)
}

func (s *deviceMgrSuite) TestDeviceManagerEnsureSeededChangeInFlight(c *C) {
	s.state.Lock()
	chg := s.state.NewChange("seed", "just for testing")
	chg.AddTask(s.state.NewTask("test-task", "the change needs a task"))
	s.state.Unlock()

	called := false
	restore := devicestate.MockPopulateStateFromSeed(s.mgr, func(string, string, timings.Measurer) ([]*state.TaskSet, error) {
		called = true
		return nil, nil
	})
	defer restore()

	err := devicestate.EnsureSeeded(s.mgr)
	c.Assert(err, IsNil)
	c.Assert(called, Equals, false)
}

func (s *deviceMgrSuite) TestDeviceManagerEnsureSeededAlsoOnClassic(c *C) {
	release.OnClassic = true

	called := false
	restore := devicestate.MockPopulateStateFromSeed(s.mgr, func(sLabel, sMode string, tm timings.Measurer) ([]*state.TaskSet, error) {
		called = true
		c.Check(sMode, Equals, "")
		c.Check(sLabel, Equals, "")
		return nil, nil
	})
	defer restore()

	err := devicestate.EnsureSeeded(s.mgr)
	c.Assert(err, IsNil)
	c.Assert(called, Equals, true)
}

func (s *deviceMgrSuite) TestDeviceManagerEnsureSeededHappy(c *C) {
	restore := devicestate.MockPopulateStateFromSeed(s.mgr, func(sLabel, sMode string, tm timings.Measurer) (ts []*state.TaskSet, err error) {
		c.Assert(sLabel, Equals, "")
		c.Assert(sMode, Equals, "")
		t := s.state.NewTask("test-task", "a random task")
		ts = append(ts, state.NewTaskSet(t))
		return ts, nil
	})
	defer restore()

	err := devicestate.EnsureSeeded(s.mgr)
	c.Assert(err, IsNil)

	s.state.Lock()
	defer s.state.Unlock()

	c.Check(s.state.Changes(), HasLen, 1)

	var seedStartTime time.Time
	c.Assert(s.state.Get("seed-start-time", &seedStartTime), IsNil)
	c.Check(seedStartTime.Equal(devicestate.StartTime()), Equals, true)
}

func (s *deviceMgrSuite) TestDeviceManagerEnsureBootOkSkippedOnClassic(c *C) {
	s.bootloader.GetErr = fmt.Errorf("should not be called")
	release.OnClassic = true

	err := devicestate.EnsureBootOk(s.mgr)
	c.Assert(err, IsNil)
}

func (s *deviceMgrSuite) TestDeviceManagerEnsureBootOkSkippedOnNonRunModes(c *C) {
	s.bootloader.GetErr = fmt.Errorf("should not be called")
	devicestate.SetSystemMode(s.mgr, "install")

	err := devicestate.EnsureBootOk(s.mgr)
	c.Assert(err, IsNil)
}

func (s *deviceMgrSuite) switchDevManagerToClassicWithModes(c *C) {
	release.OnClassic = true

	// mock the modeenv file
	m := boot.Modeenv{
		Mode: "run",
	}
	err := m.WriteTo("")
	c.Assert(err, IsNil)

	// re-create manager so that modeenv file is-read
	s.mgr, err = devicestate.Manager(s.state, s.hookMgr, s.o.TaskRunner(), s.newStore)
	c.Assert(err, IsNil)
}

func (s *deviceMgrSuite) TestDeviceManagerEnsureSeededHappyWithModeenv(c *C) {
	n := 0

	// mock the modeenv file
	m := boot.Modeenv{
		Mode:           "install",
		RecoverySystem: "20191127",
	}
	err := m.WriteTo("")
	c.Assert(err, IsNil)

	// re-create manager so that modeenv file is-read
	s.mgr, err = devicestate.Manager(s.state, s.hookMgr, s.o.TaskRunner(), s.newStore)
	c.Assert(err, IsNil)
	restore := devicestate.MockPopulateStateFromSeed(s.mgr, func(sLabel, sMode string, tm timings.Measurer) (ts []*state.TaskSet, err error) {
		c.Check(sLabel, Equals, "20191127")
		c.Check(sMode, Equals, "install")

		t := s.state.NewTask("test-task", "a random task")
		ts = append(ts, state.NewTaskSet(t))

		n++
		return ts, nil
	})
	defer restore()

	err = devicestate.EnsureSeeded(s.mgr)
	c.Assert(err, IsNil)

	s.state.Lock()
	defer s.state.Unlock()

	c.Check(s.state.Changes(), HasLen, 1)
	c.Check(n, Equals, 1)
}

func (s *deviceMgrSuite) TestDeviceManagerEnsureBootOkBootloaderHappy(c *C) {
	s.setPCModelInState(c)

	s.bootloader.SetBootVars(map[string]string{
		"snap_mode":     boot.TryingStatus,
		"snap_try_core": "core_1.snap",
	})

	s.state.Lock()
	defer s.state.Unlock()
	siCore1 := &snap.SideInfo{RealName: "core", Revision: snap.R(1)}
	snapstate.Set(s.state, "core", &snapstate.SnapState{
		SnapType: "os",
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{siCore1}),
		Current:  siCore1.Revision,
	})

	s.state.Unlock()
	err := devicestate.EnsureBootOk(s.mgr)
	s.state.Lock()
	c.Assert(err, IsNil)

	m, err := s.bootloader.GetBootVars("snap_mode")
	c.Assert(err, IsNil)
	c.Assert(m, DeepEquals, map[string]string{"snap_mode": ""})
}

func (s *deviceMgrSuite) TestDeviceManagerEnsureBootOkUpdateBootRevisionsHappy(c *C) {
	s.setPCModelInState(c)

	// simulate that we have a new core_2, tried to boot it but that failed
	s.bootloader.SetBootVars(map[string]string{
		"snap_mode":     "",
		"snap_kernel":   "kernel_1.snap",
		"snap_try_core": "core_2.snap",
		"snap_core":     "core_1.snap",
	})

	s.state.Lock()
	defer s.state.Unlock()
	siKernel1 := &snap.SideInfo{RealName: "kernel", Revision: snap.R(1)}
	snapstate.Set(s.state, "kernel", &snapstate.SnapState{
		SnapType: "kernel",
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{siKernel1}),
		Current:  siKernel1.Revision,
	})

	siCore1 := &snap.SideInfo{RealName: "core", Revision: snap.R(1)}
	siCore2 := &snap.SideInfo{RealName: "core", Revision: snap.R(2)}
	snapstate.Set(s.state, "core", &snapstate.SnapState{
		SnapType: "os",
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{siCore1, siCore2}),
		Current:  siCore2.Revision,
	})

	s.state.Unlock()
	err := devicestate.EnsureBootOk(s.mgr)
	s.state.Lock()
	c.Assert(err, IsNil)

	c.Check(s.state.Changes(), HasLen, 1)
	c.Check(s.state.Changes()[0].Kind(), Equals, "update-revisions")
}

func (s *deviceMgrSuite) TestDeviceManagerEnsureBootOkNotRunAgain(c *C) {
	s.setPCModelInState(c)

	s.bootloader.SetBootVars(map[string]string{
		"snap_mode":     boot.TryingStatus,
		"snap_try_core": "core_1.snap",
	})
	s.bootloader.SetErr = fmt.Errorf("ensure bootloader is not used")

	restore := devicestate.SetBootOkRanForCurrentBootID(s.mgr, true)
	defer restore()

	err := devicestate.EnsureBootOk(s.mgr)
	c.Assert(err, IsNil)
}

func (s *deviceMgrSuite) TestDeviceManagerEnsureBootOkError(c *C) {
	s.setPCModelInState(c)

	s.state.Lock()
	// seeded
	s.state.Set("seeded", true)
	// has serial
	devicestatetest.SetDevice(s.state, &auth.DeviceState{
		Brand:  "canonical",
		Model:  "pc",
		Serial: "8989",
	})
	s.state.Unlock()

	s.bootloader.GetErr = fmt.Errorf("bootloader err")

	restore := devicestate.SetBootOkRanForCurrentBootID(s.mgr, false)
	defer restore()

	err := s.mgr.Ensure()
	c.Assert(err, ErrorMatches, "devicemgr: cannot mark boot successful: bootloader err")
}

func fakeMyModel(extra map[string]any) *asserts.Model {
	model := map[string]any{
		"type":         "model",
		"authority-id": "my-brand",
		"series":       "16",
		"brand-id":     "my-brand",
		"model":        "my-model",
	}
	return assertstest.FakeAssertion(model, extra).(*asserts.Model)
}

func (s *deviceMgrSuite) TestCheckGadget(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	gadgetInfo := snaptest.MockInfo(c, "{type: gadget, name: other-gadget, version: 0}", nil)

	s.setupBrands()
	// model assertion in device context
	model := fakeMyModel(map[string]any{
		"architecture": "amd64",
		"gadget":       "gadget",
		"kernel":       "krnl",
	})
	deviceCtx := &snapstatetest.TrivialDeviceContext{DeviceModel: model}

	err := devicestate.CheckGadgetOrKernel(s.state, gadgetInfo, nil, nil, snapstate.Flags{}, deviceCtx)
	c.Check(err, ErrorMatches, `cannot install gadget "other-gadget", model assertion requests "gadget"`)

	// brand gadget
	brandGadgetInfo := snaptest.MockInfo(c, "{type: gadget, name: gadget, version: 0}", nil)
	brandGadgetInfo.SnapID = "brand-gadget-id"
	s.setupSnapDecl(c, brandGadgetInfo, "my-brand")

	// canonical gadget
	canonicalGadgetInfo := snaptest.MockInfo(c, "{type: gadget, name: gadget, version: 0}", nil)
	canonicalGadgetInfo.SnapID = "canonical-gadget-id"
	s.setupSnapDecl(c, canonicalGadgetInfo, "canonical")

	// other gadget
	otherGadgetInfo := snaptest.MockInfo(c, "{type: gadget, name: gadget, version: 0}", nil)
	otherGadgetInfo.SnapID = "other-gadget-id"
	s.setupSnapDecl(c, otherGadgetInfo, "other-brand")

	// install brand gadget ok
	err = devicestate.CheckGadgetOrKernel(s.state, brandGadgetInfo, nil, nil, snapstate.Flags{}, deviceCtx)
	c.Check(err, IsNil)

	// install canonical gadget ok
	err = devicestate.CheckGadgetOrKernel(s.state, canonicalGadgetInfo, nil, nil, snapstate.Flags{}, deviceCtx)
	c.Check(err, IsNil)

	// install other gadget fails
	err = devicestate.CheckGadgetOrKernel(s.state, otherGadgetInfo, nil, nil, snapstate.Flags{}, deviceCtx)
	c.Check(err, ErrorMatches, `cannot install gadget "gadget" published by "other-brand" for model by "my-brand"`)

	// unasserted installation of other works
	otherGadgetInfo.SnapID = ""
	err = devicestate.CheckGadgetOrKernel(s.state, otherGadgetInfo, nil, nil, snapstate.Flags{}, deviceCtx)
	c.Check(err, IsNil)

	// parallel install fails
	otherGadgetInfo.InstanceKey = "foo"
	err = devicestate.CheckGadgetOrKernel(s.state, otherGadgetInfo, nil, nil, snapstate.Flags{}, deviceCtx)
	c.Check(err, ErrorMatches, `cannot install "gadget_foo", parallel installation of kernel or gadget snaps is not supported`)
}

func (s *deviceMgrSuite) TestCheckGadgetOnClassic(c *C) {
	release.OnClassic = true

	s.state.Lock()
	defer s.state.Unlock()

	gadgetInfo := snaptest.MockInfo(c, "{type: gadget, name: other-gadget, version: 0}", nil)

	s.setupBrands()
	// model assertion in device context
	model := fakeMyModel(map[string]any{
		"classic": "true",
		"gadget":  "gadget",
	})
	deviceCtx := &snapstatetest.TrivialDeviceContext{DeviceModel: model}

	err := devicestate.CheckGadgetOrKernel(s.state, gadgetInfo, nil, nil, snapstate.Flags{}, deviceCtx)
	c.Check(err, ErrorMatches, `cannot install gadget "other-gadget", model assertion requests "gadget"`)

	// brand gadget
	brandGadgetInfo := snaptest.MockInfo(c, "{type: gadget, name: gadget, version: 0}", nil)
	brandGadgetInfo.SnapID = "brand-gadget-id"
	s.setupSnapDecl(c, brandGadgetInfo, "my-brand")

	// canonical gadget
	canonicalGadgetInfo := snaptest.MockInfo(c, "{type: gadget, name: gadget, version: 0}", nil)
	canonicalGadgetInfo.SnapID = "canonical-gadget-id"
	s.setupSnapDecl(c, canonicalGadgetInfo, "canonical")

	// other gadget
	otherGadgetInfo := snaptest.MockInfo(c, "{type: gadget, name: gadget, version: 0}", nil)
	otherGadgetInfo.SnapID = "other-gadget-id"
	s.setupSnapDecl(c, otherGadgetInfo, "other-brand")

	// install brand gadget ok
	err = devicestate.CheckGadgetOrKernel(s.state, brandGadgetInfo, nil, nil, snapstate.Flags{}, deviceCtx)
	c.Check(err, IsNil)

	// install canonical gadget ok
	err = devicestate.CheckGadgetOrKernel(s.state, canonicalGadgetInfo, nil, nil, snapstate.Flags{}, deviceCtx)
	c.Check(err, IsNil)

	// install other gadget fails
	err = devicestate.CheckGadgetOrKernel(s.state, otherGadgetInfo, nil, nil, snapstate.Flags{}, deviceCtx)
	c.Check(err, ErrorMatches, `cannot install gadget "gadget" published by "other-brand" for model by "my-brand"`)

	// unasserted installation of other works
	otherGadgetInfo.SnapID = ""
	err = devicestate.CheckGadgetOrKernel(s.state, otherGadgetInfo, nil, nil, snapstate.Flags{}, deviceCtx)
	c.Check(err, IsNil)
}

func (s *deviceMgrSuite) TestCheckGadgetOnClassicGadgetNotSpecified(c *C) {
	release.OnClassic = true

	s.state.Lock()
	defer s.state.Unlock()

	gadgetInfo := snaptest.MockInfo(c, "{type: gadget, name: gadget, version: 0}", nil)

	s.setupBrands()
	// model assertion in device context
	model := fakeMyModel(map[string]any{
		"classic": "true",
	})
	deviceCtx := &snapstatetest.TrivialDeviceContext{DeviceModel: model}

	err := devicestate.CheckGadgetOrKernel(s.state, gadgetInfo, nil, nil, snapstate.Flags{}, deviceCtx)
	c.Check(err, ErrorMatches, `cannot install gadget snap on classic if not requested by the model`)
}

func (s *deviceMgrSuite) TestCheckGadgetValid(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	// model assertion in device context
	model := fakeMyModel(map[string]any{
		"architecture": "amd64",
		"gadget":       "gadget",
		"kernel":       "krnl",
	})
	deviceCtx := &snapstatetest.TrivialDeviceContext{DeviceModel: model}

	gadgetInfo := snaptest.MockInfo(c, "{type: gadget, name: gadget, version: 0}", nil)

	// valid gadget.yaml
	cont := snaptest.MockContainer(c, [][]string{
		{"meta/gadget.yaml", gadgetYaml},
	})
	err := devicestate.CheckGadgetValid(s.state, gadgetInfo, nil, cont, snapstate.Flags{}, deviceCtx)
	c.Check(err, IsNil)

	// invalid gadget.yaml
	cont = snaptest.MockContainer(c, [][]string{
		{"meta/gadget.yaml", `defaults:`},
	})
	err = devicestate.CheckGadgetValid(s.state, gadgetInfo, nil, cont, snapstate.Flags{}, deviceCtx)
	c.Check(err, ErrorMatches, `bootloader not declared in any volume`)

}

func (s *deviceMgrSuite) TestCheckKernel(c *C) {
	s.state.Lock()
	defer s.state.Unlock()
	kernelInfo := snaptest.MockInfo(c, "{type: kernel, name: lnrk, version: 0}", nil)

	// not on classic without modes
	release.OnClassic = true
	// model assertion in device context
	model := fakeMyModel(map[string]any{
		"classic": "true",
	})
	deviceCtx := &snapstatetest.TrivialDeviceContext{DeviceModel: model}
	err := devicestate.CheckGadgetOrKernel(s.state, kernelInfo, nil, nil, snapstate.Flags{}, deviceCtx)
	c.Check(err, ErrorMatches, `cannot install a kernel snap if classic boot`)
	release.OnClassic = false

	s.setupBrands()
	// model assertion in device context
	model = fakeMyModel(map[string]any{
		"architecture": "amd64",
		"gadget":       "gadget",
		"kernel":       "krnl",
	})
	deviceCtx = &snapstatetest.TrivialDeviceContext{DeviceModel: model}

	err = devicestate.CheckGadgetOrKernel(s.state, kernelInfo, nil, nil, snapstate.Flags{}, deviceCtx)
	c.Check(err, ErrorMatches, `cannot install kernel "lnrk", model assertion requests "krnl"`)

	// brand kernel
	brandKrnlInfo := snaptest.MockInfo(c, "{type: kernel, name: krnl, version: 0}", nil)
	brandKrnlInfo.SnapID = "brand-krnl-id"
	s.setupSnapDecl(c, brandKrnlInfo, "my-brand")

	// canonical kernel
	canonicalKrnlInfo := snaptest.MockInfo(c, "{type: kernel, name: krnl, version: 0}", nil)
	canonicalKrnlInfo.SnapID = "canonical-krnl-id"
	s.setupSnapDecl(c, canonicalKrnlInfo, "canonical")

	// other kernel
	otherKrnlInfo := snaptest.MockInfo(c, "{type: kernel, name: krnl, version: 0}", nil)
	otherKrnlInfo.SnapID = "other-krnl-id"
	s.setupSnapDecl(c, otherKrnlInfo, "other-brand")

	// install brand kernel ok
	err = devicestate.CheckGadgetOrKernel(s.state, brandKrnlInfo, nil, nil, snapstate.Flags{}, deviceCtx)
	c.Check(err, IsNil)

	// install canonical kernel ok
	err = devicestate.CheckGadgetOrKernel(s.state, canonicalKrnlInfo, nil, nil, snapstate.Flags{}, deviceCtx)
	c.Check(err, IsNil)

	// install other kernel fails
	err = devicestate.CheckGadgetOrKernel(s.state, otherKrnlInfo, nil, nil, snapstate.Flags{}, deviceCtx)
	c.Check(err, ErrorMatches, `cannot install kernel "krnl" published by "other-brand" for model by "my-brand"`)

	// unasserted installation of other works
	otherKrnlInfo.SnapID = ""
	err = devicestate.CheckGadgetOrKernel(s.state, otherKrnlInfo, nil, nil, snapstate.Flags{}, deviceCtx)
	c.Check(err, IsNil)

	// parallel install fails
	otherKrnlInfo.InstanceKey = "foo"
	err = devicestate.CheckGadgetOrKernel(s.state, otherKrnlInfo, nil, nil, snapstate.Flags{}, deviceCtx)
	c.Check(err, ErrorMatches, `cannot install "krnl_foo", parallel installation of kernel or gadget snaps is not supported`)
}

func (s *deviceMgrSuite) TestCheckKernelOnClassicWithModes(c *C) {
	s.switchDevManagerToClassicWithModes(c)

	s.state.Lock()
	defer s.state.Unlock()
	kernelInfo := snaptest.MockInfo(c, "{type: kernel, name: pc-kernel, version: 0}", nil)

	// model assertion in device context
	model := fakeMyModel(map[string]any{
		"architecture": "amd64",
		"classic":      "true",
		"grade":        "dangerous",
		"distribution": "ubuntu",
		"base":         "core22",
		"snaps": []any{
			map[string]any{
				"name":            "pc-kernel",
				"id":              snaptest.AssertedSnapID("pc-kernel"),
				"type":            "kernel",
				"default-channel": "22",
			},
			map[string]any{
				"name":            "pc",
				"id":              snaptest.AssertedSnapID("pc"),
				"type":            "gadget",
				"default-channel": "22",
			},
		},
	})
	deviceCtx := &snapstatetest.TrivialDeviceContext{DeviceModel: model}
	err := devicestate.CheckGadgetOrKernel(s.state, kernelInfo, nil, nil, snapstate.Flags{}, deviceCtx)
	c.Check(err, IsNil)
}

func (s *deviceMgrSuite) TestCanAutoRefreshOnCore(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	canAutoRefresh := func() bool {
		ok, err := devicestate.CanAutoRefresh(s.state)
		c.Assert(err, IsNil)
		return ok
	}

	// not seeded, no model, no serial -> no auto-refresh
	s.state.Set("seeded", false)
	c.Check(canAutoRefresh(), Equals, false)

	// seeded, model, no serial -> no auto-refresh
	s.state.Set("seeded", true)
	devicestatetest.SetDevice(s.state, &auth.DeviceState{
		Brand: "canonical",
		Model: "pc",
	})
	s.makeModelAssertionInState(c, "canonical", "pc", map[string]any{
		"architecture": "amd64",
		"kernel":       "pc-kernel",
		"gadget":       "pc",
	})
	c.Check(canAutoRefresh(), Equals, false)

	// seeded, model, serial -> auto-refresh
	devicestatetest.SetDevice(s.state, &auth.DeviceState{
		Brand:  "canonical",
		Model:  "pc",
		Serial: "8989",
	})
	s.makeSerialAssertionInState(c, "canonical", "pc", "8989")
	c.Check(canAutoRefresh(), Equals, true)

	// not seeded, model, serial -> no auto-refresh
	s.state.Set("seeded", false)
	c.Check(canAutoRefresh(), Equals, false)
}

func (s *deviceMgrSuite) TestCanAutoRefreshNoSerialFallback(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	canAutoRefresh := func() bool {
		ok, err := devicestate.CanAutoRefresh(s.state)
		c.Assert(err, IsNil)
		return ok
	}

	// seeded, model, no serial, two attempts at getting serial
	// -> no auto-refresh
	devicestate.IncEnsureOperationalAttempts(s.state)
	devicestate.IncEnsureOperationalAttempts(s.state)
	s.state.Set("seeded", true)
	devicestatetest.SetDevice(s.state, &auth.DeviceState{
		Brand: "canonical",
		Model: "pc",
	})
	s.makeModelAssertionInState(c, "canonical", "pc", map[string]any{
		"architecture": "amd64",
		"kernel":       "pc-kernel",
		"gadget":       "pc",
	})
	c.Check(canAutoRefresh(), Equals, false)

	// third attempt ongoing, or done
	// fallback, try auto-refresh
	devicestate.IncEnsureOperationalAttempts(s.state)
	// validity
	c.Check(devicestate.EnsureOperationalAttempts(s.state), Equals, 3)
	c.Check(canAutoRefresh(), Equals, true)
}

func (s *deviceMgrSuite) TestCanAutoRefreshOnClassic(c *C) {
	release.OnClassic = true

	s.state.Lock()
	defer s.state.Unlock()

	canAutoRefresh := func() bool {
		ok, err := devicestate.CanAutoRefresh(s.state)
		c.Assert(err, IsNil)
		return ok
	}

	// not seeded, no model, no serial -> no auto-refresh
	s.state.Set("seeded", false)
	c.Check(canAutoRefresh(), Equals, false)

	// seeded, no model -> auto-refresh
	s.state.Set("seeded", true)
	c.Check(canAutoRefresh(), Equals, false)

	// seeded, model, no serial -> no auto-refresh
	devicestatetest.SetDevice(s.state, &auth.DeviceState{
		Brand: "canonical",
		Model: "pc",
	})
	s.makeModelAssertionInState(c, "canonical", "pc", map[string]any{
		"classic": "true",
	})
	c.Check(canAutoRefresh(), Equals, false)

	// seeded, model, serial -> auto-refresh
	devicestatetest.SetDevice(s.state, &auth.DeviceState{
		Brand:  "canonical",
		Model:  "pc",
		Serial: "8989",
	})
	s.makeSerialAssertionInState(c, "canonical", "pc", "8989")
	c.Check(canAutoRefresh(), Equals, true)

	// not seeded, model, serial -> no auto-refresh
	s.state.Set("seeded", false)
	c.Check(canAutoRefresh(), Equals, false)
}

func makeInstalledMockCoreSnapWithSnapdControl(c *C, st *state.State) *snap.Info {
	sideInfoCore11 := &snap.SideInfo{RealName: "core", Revision: snap.R(11), SnapID: "core-id"}
	snapstate.Set(st, "core", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{sideInfoCore11}),
		Current:  sideInfoCore11.Revision,
		SnapType: "os",
	})
	core11 := snaptest.MockSnap(c, `
name: core
version: 1.0
slots:
 snapd-control:
`, sideInfoCore11)
	c.Assert(core11.Slots, HasLen, 1)

	return core11
}

var snapWithSnapdControlRefreshScheduleManagedYAML = `
name: snap-with-snapd-control
version: 1.0
plugs:
 snapd-control:
  refresh-schedule: managed
`

var snapWithSnapdControlOnlyYAML = `
name: snap-with-snapd-control
version: 1.0
plugs:
 snapd-control:
`

func makeInstalledMockSnap(c *C, st *state.State, yml string) *snap.Info {
	sideInfo11 := &snap.SideInfo{RealName: "snap-with-snapd-control", Revision: snap.R(11), SnapID: "snap-with-snapd-control-id"}
	snapstate.Set(st, "snap-with-snapd-control", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{sideInfo11}),
		Current:  sideInfo11.Revision,
		SnapType: "app",
	})
	info11 := snaptest.MockSnap(c, yml, sideInfo11)
	c.Assert(info11.Plugs, HasLen, 1)

	return info11
}

func makeInstalledMockKernelSnap(c *C, st *state.State, yml string) *snap.Info {
	sideInfo11 := &snap.SideInfo{RealName: "pc-kernel", Revision: snap.R(11), SnapID: "pc-kernel-id"}
	snapstate.Set(st, "pc-kernel", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{sideInfo11}),
		Current:  sideInfo11.Revision,
		SnapType: "kernel",
	})
	info11 := snaptest.MockSnap(c, yml, sideInfo11)

	return info11
}

func makeMockRepoWithConnectedSnaps(c *C, st *state.State, info11, core11 *snap.Info, ifname string) {
	repo := interfaces.NewRepository()
	for _, iface := range builtin.Interfaces() {
		err := repo.AddInterface(iface)
		c.Assert(err, IsNil)
	}

	info11AppSet, err := interfaces.NewSnapAppSet(info11, nil)
	c.Assert(err, IsNil)

	err = repo.AddAppSet(info11AppSet)
	c.Assert(err, IsNil)

	core11AppSet, err := interfaces.NewSnapAppSet(core11, nil)
	c.Assert(err, IsNil)

	err = repo.AddAppSet(core11AppSet)
	c.Assert(err, IsNil)

	_, err = repo.Connect(&interfaces.ConnRef{
		PlugRef: interfaces.PlugRef{Snap: info11.InstanceName(), Name: ifname},
		SlotRef: interfaces.SlotRef{Snap: core11.InstanceName(), Name: ifname},
	}, nil, nil, nil, nil, nil)
	c.Assert(err, IsNil)
	conns, err := repo.Connected("snap-with-snapd-control", "snapd-control")
	c.Assert(err, IsNil)
	c.Assert(conns, HasLen, 1)
	ifacerepo.Replace(st, repo)
}

func (s *deviceMgrSuite) TestCanManageRefreshes(c *C) {
	st := s.state
	st.Lock()
	defer st.Unlock()

	// not possbile to manage by default
	c.Check(devicestate.CanManageRefreshes(st), Equals, false)

	// not possible with just a snap with "snapd-control" plug with the
	// right attribute
	info11 := makeInstalledMockSnap(c, st, snapWithSnapdControlRefreshScheduleManagedYAML)
	c.Check(devicestate.CanManageRefreshes(st), Equals, false)

	// not possible with a core snap with snapd control
	core11 := makeInstalledMockCoreSnapWithSnapdControl(c, st)
	c.Check(devicestate.CanManageRefreshes(st), Equals, false)

	// not possible even with connected interfaces
	makeMockRepoWithConnectedSnaps(c, st, info11, core11, "snapd-control")
	c.Check(devicestate.CanManageRefreshes(st), Equals, false)

	// if all of the above plus a snap declaration are in place we can
	// manage schedules
	s.setupSnapDecl(c, info11, "canonical")
	c.Check(devicestate.CanManageRefreshes(st), Equals, true)

	// works if the snap is not active as well (to fix race when a
	// snap is refreshed)
	var sideInfo11 snapstate.SnapState
	err := snapstate.Get(st, "snap-with-snapd-control", &sideInfo11)
	c.Assert(err, IsNil)
	sideInfo11.Active = false
	snapstate.Set(st, "snap-with-snapd-control", &sideInfo11)
	c.Check(devicestate.CanManageRefreshes(st), Equals, true)
}

func (s *deviceMgrSuite) TestCanManageRefreshesNoRefreshScheduleManaged(c *C) {
	st := s.state
	st.Lock()
	defer st.Unlock()

	// just having a connected "snapd-control" interface is not enough
	// for setting refresh.schedule=managed
	info11 := makeInstalledMockSnap(c, st, snapWithSnapdControlOnlyYAML)
	core11 := makeInstalledMockCoreSnapWithSnapdControl(c, st)
	makeMockRepoWithConnectedSnaps(c, st, info11, core11, "snapd-control")
	s.setupSnapDecl(c, info11, "canonical")

	c.Check(devicestate.CanManageRefreshes(st), Equals, false)
}

func (s *deviceMgrSuite) TestResetSession(c *C) {
	st := state.New(nil)
	st.Lock()
	defer st.Unlock()

	c.Assert(devicestate.ResetSession(st), IsNil)

	devicestatetest.SetDevice(st, &auth.DeviceState{
		SessionMacaroon: "session",
	})
	c.Assert(devicestate.ResetSession(st), IsNil)

	device, err := devicestatetest.Device(st)
	c.Assert(err, IsNil)
	c.Check(device.SessionMacaroon, Equals, "")
}

func (s *deviceMgrSuite) TestReloadRegistered(c *C) {
	st := state.New(nil)

	runner1 := state.NewTaskRunner(st)
	hookMgr1, err := hookstate.Manager(st, runner1)
	c.Assert(err, IsNil)
	mgr1, err := devicestate.Manager(st, hookMgr1, runner1, nil)
	c.Assert(err, IsNil)

	ok := false
	select {
	case <-mgr1.Registered():
	default:
		ok = true
	}
	c.Check(ok, Equals, true)

	st.Lock()
	devicestatetest.SetDevice(st, &auth.DeviceState{
		Brand:  "canonical",
		Model:  "pc",
		Serial: "serial",
	})
	st.Unlock()

	runner2 := state.NewTaskRunner(st)
	hookMgr2, err := hookstate.Manager(st, runner2)
	c.Assert(err, IsNil)
	mgr2, err := devicestate.Manager(st, hookMgr2, runner2, nil)
	c.Assert(err, IsNil)

	ok = false
	select {
	case <-mgr2.Registered():
		ok = true
	case <-time.After(5 * time.Second):
		c.Fatal("should have been marked registered")
	}
	c.Check(ok, Equals, true)
}

func (s *deviceMgrSuite) TestMarkSeededInConfig(c *C) {
	st := s.state
	st.Lock()
	defer st.Unlock()

	// avoid device registration
	devicestatetest.SetDevice(s.state, &auth.DeviceState{
		Serial: "123",
	})

	// avoid full seeding
	s.seeding()

	// not seeded -> no config is set
	s.state.Unlock()
	s.mgr.Ensure()
	s.state.Lock()

	var seedLoaded bool
	tr := config.NewTransaction(st)
	tr.Get("core", "seed.loaded", &seedLoaded)
	c.Check(seedLoaded, Equals, false)

	// pretend we are seeded now
	s.state.Set("seeded", true)

	// seeded -> config got updated
	s.state.Unlock()
	s.mgr.Ensure()
	s.state.Lock()

	tr = config.NewTransaction(st)
	tr.Get("core", "seed.loaded", &seedLoaded)
	c.Check(seedLoaded, Equals, true)

	// only the fake seeding change is in the state, no further
	// changes
	c.Check(s.state.Changes(), HasLen, 1)
}

func (s *deviceMgrSuite) TestDevicemgrCanStandby(c *C) {
	st := state.New(nil)

	runner := state.NewTaskRunner(st)
	hookMgr, err := hookstate.Manager(st, runner)
	c.Assert(err, IsNil)
	mgr, err := devicestate.Manager(st, hookMgr, runner, nil)
	c.Assert(err, IsNil)

	st.Lock()
	defer st.Unlock()
	c.Check(mgr.CanStandby(), Equals, false)

	st.Set("seeded", true)
	c.Check(mgr.CanStandby(), Equals, true)
}

func (s *deviceMgrSuite) TestDeviceManagerReadsModeenv(c *C) {
	modeEnv := &boot.Modeenv{Mode: "install"}
	err := modeEnv.WriteTo("")
	c.Assert(err, IsNil)

	runner := s.o.TaskRunner()
	mgr, err := devicestate.Manager(s.state, s.hookMgr, runner, s.newStore)
	c.Assert(err, IsNil)
	c.Assert(mgr, NotNil)
	c.Assert(mgr.SystemMode(devicestate.SysAny), Equals, "install")
	c.Assert(mgr.SystemMode(devicestate.SysHasModeenv), Equals, "install")
}

func (s *deviceMgrSuite) TestDeviceManagerEmptySystemModeRun(c *C) {
	// set empty system mode
	devicestate.SetSystemMode(s.mgr, "")

	// empty is returned as "run" for SysAny
	c.Check(s.mgr.SystemMode(devicestate.SysAny), Equals, "run")
	// empty is returned as itself for SysHasModeenv
	c.Check(s.mgr.SystemMode(devicestate.SysHasModeenv), Equals, "")
}

func (s *deviceMgrSuite) TestDeviceManagerSystemModeInfoTooEarly(c *C) {
	runner := s.o.TaskRunner()
	mgr, err := devicestate.Manager(s.state, s.hookMgr, runner, s.newStore)
	c.Assert(err, IsNil)

	s.state.Lock()
	defer s.state.Unlock()

	_, err = mgr.SystemModeInfo()
	c.Check(err, ErrorMatches, `cannot report system mode information before device model is acknowledged`)
}

func (s *deviceMgrSuite) TestDeviceManagerSystemModeInfoUC18(c *C) {
	runner := s.o.TaskRunner()
	mgr, err := devicestate.Manager(s.state, s.hookMgr, runner, s.newStore)
	c.Assert(err, IsNil)

	s.state.Lock()
	defer s.state.Unlock()

	// have a model
	s.makeModelAssertionInState(c, "canonical", "pc", map[string]any{
		"architecture": "amd64",
		"kernel":       "pc-kernel",
		"gadget":       "pc",
		"base:":        "core18",
	})

	devicestatetest.SetDevice(s.state, &auth.DeviceState{
		Brand: "canonical",
		Model: "pc",
	})

	smi, err := mgr.SystemModeInfo()
	c.Assert(err, IsNil)
	c.Check(smi, DeepEquals, &devicestate.SystemModeInfo{
		Mode:   "run",
		Seeded: false,
	})

	// seeded
	s.state.Set("seeded", true)

	smi, err = mgr.SystemModeInfo()
	c.Assert(err, IsNil)
	c.Check(smi, DeepEquals, &devicestate.SystemModeInfo{
		Mode:   "run",
		Seeded: true,
	})
}

func (s *deviceMgrSuite) TestDeviceManagerSystemModeInfoUC20Install(c *C) {
	modeEnv := &boot.Modeenv{Mode: "install"}
	err := modeEnv.WriteTo("")
	c.Assert(err, IsNil)

	runner := s.o.TaskRunner()
	mgr, err := devicestate.Manager(s.state, s.hookMgr, runner, s.newStore)
	c.Assert(err, IsNil)

	s.setUC20PCModelInState(c)

	s.state.Lock()
	defer s.state.Unlock()

	// seeded
	s.state.Set("seeded", true)
	// no flags
	c.Assert(boot.InitramfsExposeBootFlagsForSystem(nil), IsNil)
	// data present
	ubuntuData := filepath.Dir(filepath.Join(dirs.GlobalRootDir, "/run/mnt/ubuntu-data/system-data"))
	c.Assert(os.MkdirAll(ubuntuData, 0755), IsNil)

	smi, err := mgr.SystemModeInfo()
	c.Assert(err, IsNil)
	c.Check(smi, DeepEquals, &devicestate.SystemModeInfo{
		Mode:              "install",
		HasModeenv:        true,
		Seeded:            true,
		BootFlags:         []string{},
		HostDataLocations: []string{ubuntuData},
	})

	// factory
	c.Assert(boot.InitramfsExposeBootFlagsForSystem([]string{"factory"}), IsNil)
	smi, err = mgr.SystemModeInfo()
	c.Assert(err, IsNil)
	c.Check(smi, DeepEquals, &devicestate.SystemModeInfo{
		Mode:              "install",
		HasModeenv:        true,
		Seeded:            true,
		BootFlags:         []string{"factory"},
		HostDataLocations: []string{ubuntuData},
	})
}

func (s *deviceMgrSuite) TestDeviceManagerSystemModeInfoUC20Run(c *C) {
	modeEnv := &boot.Modeenv{Mode: "run"}
	err := modeEnv.WriteTo("")
	c.Assert(err, IsNil)

	runner := s.o.TaskRunner()
	mgr, err := devicestate.Manager(s.state, s.hookMgr, runner, s.newStore)
	c.Assert(err, IsNil)

	s.setUC20PCModelInState(c)

	s.state.Lock()
	defer s.state.Unlock()

	// not seeded
	// no flags
	c.Assert(boot.InitramfsExposeBootFlagsForSystem(nil), IsNil)

	smi, err := mgr.SystemModeInfo()
	c.Assert(err, IsNil)
	c.Check(smi, DeepEquals, &devicestate.SystemModeInfo{
		Mode:              "run",
		HasModeenv:        true,
		Seeded:            false,
		BootFlags:         []string{},
		HostDataLocations: []string{boot.InitramfsDataDir, dirs.GlobalRootDir},
	})

	// given state only
	smi, err = devicestate.SystemModeInfoFromState(s.state)
	c.Assert(err, IsNil)
	c.Check(smi, DeepEquals, &devicestate.SystemModeInfo{
		Mode:              "run",
		HasModeenv:        true,
		Seeded:            false,
		BootFlags:         []string{},
		HostDataLocations: []string{boot.InitramfsDataDir, dirs.GlobalRootDir},
	})
}

const (
	mountRunMntUbuntuSaveFmt = `26 27 8:3 / %s/run/mnt/ubuntu-save rw,relatime shared:7 - ext4 /dev/fakedevice0p1 rw,data=ordered`
	mountSnapSaveFmt         = `26 27 8:3 / %s/var/lib/snapd/save rw,relatime shared:7 - ext4 /dev/fakedevice0p1 rw,data=ordered`
)

func (s *deviceMgrSuite) TestDeviceManagerStartupUC20UbuntuSaveRunModeHappy(c *C) {
	modeEnv := &boot.Modeenv{Mode: "run"}
	err := modeEnv.WriteTo("")
	c.Assert(err, IsNil)
	s.setUC20PCModelInState(c)

	// create a new manager so that the modeenv we mocked in read
	mgr, err := devicestate.Manager(s.state, s.hookMgr, s.o.TaskRunner(), s.newStore)
	c.Assert(err, IsNil)

	// make this one fail so we test both are invoked
	sysctlCmd := testutil.MockCommand(c, "systemctl", "echo 'Failed to start var-lib-snapd-save.mount: Unit var-lib-snapd-save.mount not found.'; exit 1")
	defer sysctlCmd.Restore()

	mountCmd := testutil.MockCommand(c, "systemd-mount", "")
	defer mountCmd.Restore()

	// ubuntu-save not mounted
	err = mgr.StartUp()
	c.Assert(err, IsNil)
	c.Check(sysctlCmd.Calls(), HasLen, 0)
	c.Check(mountCmd.Calls(), HasLen, 0)

	restore := osutil.MockMountInfo(fmt.Sprintf(mountRunMntUbuntuSaveFmt, dirs.GlobalRootDir))
	defer restore()

	err = mgr.StartUp()
	c.Assert(err, IsNil)
	c.Check(sysctlCmd.Calls(), DeepEquals, [][]string{
		{"systemctl", "start", "var-lib-snapd-save.mount"},
	})
	c.Check(mountCmd.Calls(), DeepEquals, [][]string{
		{"systemd-mount", "-o", "bind", boot.InitramfsUbuntuSaveDir, dirs.SnapSaveDir},
	})

	// known as available
	c.Check(devicestate.SaveAvailable(mgr), Equals, true)
}

func (s *deviceMgrSuite) TestDeviceManagerStartupUC20UbuntuSaveSystemCtlFails(c *C) {
	modeEnv := &boot.Modeenv{Mode: "run"}
	err := modeEnv.WriteTo("")
	c.Assert(err, IsNil)
	s.setUC20PCModelInState(c)

	// create a new manager so that the modeenv we mocked in read
	mgr, err := devicestate.Manager(s.state, s.hookMgr, s.o.TaskRunner(), s.newStore)
	c.Assert(err, IsNil)

	// this one now fails without a known reason, systemd-mount should not
	// be invoked, and we should receive an error
	sysctlCmd := testutil.MockCommand(c, "systemctl", "echo failed; exit 1")
	defer sysctlCmd.Restore()

	mountCmd := testutil.MockCommand(c, "systemd-mount", "")
	defer mountCmd.Restore()

	restore := osutil.MockMountInfo(fmt.Sprintf(mountRunMntUbuntuSaveFmt, dirs.GlobalRootDir))
	defer restore()

	err = mgr.StartUp()
	c.Assert(err, NotNil)
	c.Check(err.Error(), Equals, "cannot set up ubuntu-save: systemctl command [start var-lib-snapd-save.mount] failed with exit status 1: failed")
	c.Check(sysctlCmd.Calls(), DeepEquals, [][]string{
		{"systemctl", "start", "var-lib-snapd-save.mount"},
	})
	c.Check(mountCmd.Calls(), HasLen, 0)

	// since the systemctl fails, and it was not due to the missing mount
	// unit, then we should get an error, and ubuntu save should not bge available
	c.Check(devicestate.SaveAvailable(mgr), Equals, false)
}

func (s *deviceMgrSuite) TestDeviceManagerStartupUC20UbuntuSaveMountUnitExists(c *C) {
	modeEnv := &boot.Modeenv{Mode: "run"}
	err := modeEnv.WriteTo("")
	c.Assert(err, IsNil)
	s.setUC20PCModelInState(c)

	// create a new manager so that the modeenv we mocked in read
	mgr, err := devicestate.Manager(s.state, s.hookMgr, s.o.TaskRunner(), s.newStore)
	c.Assert(err, IsNil)

	// this one now succeeds, which means systemd-mount should not
	// be invoked
	sysctlCmd := testutil.MockCommand(c, "systemctl", "")
	defer sysctlCmd.Restore()

	mountCmd := testutil.MockCommand(c, "systemd-mount", "")
	defer mountCmd.Restore()

	restore := osutil.MockMountInfo(fmt.Sprintf(mountRunMntUbuntuSaveFmt, dirs.GlobalRootDir))
	defer restore()

	err = mgr.StartUp()
	c.Assert(err, IsNil)
	c.Check(sysctlCmd.Calls(), DeepEquals, [][]string{
		{"systemctl", "start", "var-lib-snapd-save.mount"},
	})
	c.Check(mountCmd.Calls(), HasLen, 0)

	// known as available
	c.Check(devicestate.SaveAvailable(mgr), Equals, true)
}

func (s *deviceMgrSuite) TestDeviceManagerStartupUC20UbuntuSaveAlreadyMounted(c *C) {
	modeEnv := &boot.Modeenv{Mode: "run"}
	err := modeEnv.WriteTo("")
	c.Assert(err, IsNil)
	s.setUC20PCModelInState(c)

	// create a new manager so that the modeenv we mocked in read
	mgr, err := devicestate.Manager(s.state, s.hookMgr, s.o.TaskRunner(), s.newStore)
	c.Assert(err, IsNil)

	sysctlCmd := testutil.MockCommand(c, "systemctl", "")
	defer sysctlCmd.Restore()

	mountCmd := testutil.MockCommand(c, "systemd-mount", "")
	defer mountCmd.Restore()

	// already mounted
	restore := osutil.MockMountInfo(fmt.Sprintf(mountRunMntUbuntuSaveFmt, dirs.GlobalRootDir) + "\n" +
		fmt.Sprintf(mountSnapSaveFmt, dirs.GlobalRootDir))
	defer restore()

	err = mgr.StartUp()
	c.Assert(err, IsNil)
	c.Check(sysctlCmd.Calls(), HasLen, 0)
	c.Check(mountCmd.Calls(), HasLen, 0)

	// known as available
	c.Check(devicestate.SaveAvailable(mgr), Equals, true)
}

func (s *deviceMgrSuite) TestDeviceManagerStartupUC20NoUbuntuSave(c *C) {
	modeEnv := &boot.Modeenv{Mode: "run"}
	err := modeEnv.WriteTo("")
	c.Assert(err, IsNil)
	s.setUC20PCModelInState(c)

	// create a new manager so that the modeenv we mocked in read
	mgr, err := devicestate.Manager(s.state, s.hookMgr, s.o.TaskRunner(), s.newStore)
	c.Assert(err, IsNil)

	cmd := testutil.MockCommand(c, "systemd-mount", "")
	defer cmd.Restore()

	// ubuntu-save not mounted
	err = mgr.StartUp()
	c.Assert(err, IsNil)
	c.Check(cmd.Calls(), HasLen, 0)

	// known as available
	c.Check(devicestate.SaveAvailable(mgr), Equals, true)
}

func (s *deviceMgrSuite) TestDeviceManagerStartupUC20UbuntuSaveErr(c *C) {
	modeEnv := &boot.Modeenv{Mode: "run"}
	err := modeEnv.WriteTo("")
	c.Assert(err, IsNil)
	s.setUC20PCModelInState(c)

	// create a new manager so that the modeenv we mocked in read
	mgr, err := devicestate.Manager(s.state, s.hookMgr, s.o.TaskRunner(), s.newStore)
	c.Assert(err, IsNil)

	sysctlCmd := testutil.MockCommand(c, "systemctl", "echo 'Failed to start var-lib-snapd-save.mount: Unit var-lib-snapd-save.mount not found.'; exit 1")
	defer sysctlCmd.Restore()

	mountCmd := testutil.MockCommand(c, "systemd-mount", "echo failed; exit 1")
	defer mountCmd.Restore()

	restore := osutil.MockMountInfo(fmt.Sprintf(mountRunMntUbuntuSaveFmt, dirs.GlobalRootDir))
	defer restore()

	err = mgr.StartUp()
	c.Assert(err, ErrorMatches, "cannot set up ubuntu-save: cannot bind mount .*/run/mnt/ubuntu-save under .*/var/lib/snapd/save: failed")
	c.Check(sysctlCmd.Calls(), DeepEquals, [][]string{
		{"systemctl", "start", "var-lib-snapd-save.mount"},
	})
	c.Check(mountCmd.Calls(), DeepEquals, [][]string{
		{"systemd-mount", "-o", "bind", boot.InitramfsUbuntuSaveDir, dirs.SnapSaveDir},
	})

	// known as not available
	c.Check(devicestate.SaveAvailable(mgr), Equals, false)
}

func (s *deviceMgrSuite) TestDeviceManagerStartupNonUC20NoUbuntuSave(c *C) {
	err := os.RemoveAll(dirs.SnapModeenvFileUnder(dirs.GlobalRootDir))
	c.Assert(err, IsNil)
	// create a new manager so that we know it does not see the modeenv
	mgr, err := devicestate.Manager(s.state, s.hookMgr, s.o.TaskRunner(), s.newStore)
	c.Assert(err, IsNil)

	sysctlCmd := testutil.MockCommand(c, "systemctl", "")
	defer sysctlCmd.Restore()

	mountCmd := testutil.MockCommand(c, "systemd-mount", "")
	defer mountCmd.Restore()

	// ubuntu-save not mounted
	err = mgr.StartUp()
	c.Assert(err, IsNil)
	c.Check(sysctlCmd.Calls(), HasLen, 0)
	c.Check(mountCmd.Calls(), HasLen, 0)

	// known as not available
	c.Check(devicestate.SaveAvailable(mgr), Equals, false)
}

var kernelYamlNoFdeSetup = `name: pc-kernel
version: 1.0
type: kernel
`

var kernelYamlWithFdeSetup = `name: pc-kernel
version: 1.0
type: kernel
hooks:
 fde-setup:
`

func (s *deviceMgrSuite) TestHookKeyProtectorFactory(c *C) {
	st := s.state
	st.Lock()
	defer st.Unlock()

	s.makeModelAssertionInState(c, "canonical", "pc", map[string]any{
		"architecture": "amd64",
		"kernel":       "pc-kernel",
		"gadget":       "pc",
	})
	devicestatetest.SetDevice(s.state, &auth.DeviceState{
		Brand: "canonical",
		Model: "pc",
	})

	for _, tc := range []struct {
		kernelYaml      string
		hasFdeSetupHook bool
	}{
		{kernelYamlNoFdeSetup, false},
		{kernelYamlWithFdeSetup, true},
	} {
		makeInstalledMockKernelSnap(c, st, tc.kernelYaml)

		_, err := devicestate.DeviceManagerHookKeyProtectorFactory(s.mgr, nil)
		if tc.hasFdeSetupHook {
			c.Assert(err, IsNil)
		} else {
			c.Assert(err, testutil.ErrorIs, secboot.ErrNoKeyProtector)
		}
	}
}

func (s *deviceMgrSuite) TestHasFdeSetupHookOtherKernel(c *C) {
	st := s.state
	st.Lock()
	defer st.Unlock()

	s.makeModelAssertionInState(c, "canonical", "pc", map[string]any{
		"architecture": "amd64",
		"kernel":       "pc-kernel",
		"gadget":       "pc",
	})
	devicestatetest.SetDevice(s.state, &auth.DeviceState{
		Brand: "canonical",
		Model: "pc",
	})

	otherSI := &snap.SideInfo{RealName: "pc-kernel", Revision: snap.R(1), SnapID: "pc-kernel-id"}
	_, otherInfo := snaptest.MakeTestSnapInfoWithFiles(c, kernelYamlWithFdeSetup, nil, otherSI)
	makeInstalledMockKernelSnap(c, st, kernelYamlNoFdeSetup)

	_, err := devicestate.DeviceManagerHookKeyProtectorFactory(s.mgr, nil)
	c.Assert(err, testutil.ErrorIs, secboot.ErrNoKeyProtector)

	_, err = devicestate.DeviceManagerHookKeyProtectorFactory(s.mgr, otherInfo)
	c.Assert(err, IsNil)
}

func (s *deviceMgrSuite) TestRunFDESetupHookHappy(c *C) {
	st := s.state

	st.Lock()
	makeInstalledMockKernelSnap(c, st, kernelYamlWithFdeSetup)
	s.makeModelAssertionInState(c, "canonical", "pc", map[string]any{
		"architecture": "amd64",
		"kernel":       "pc-kernel",
		"gadget":       "pc",
	})
	devicestatetest.SetDevice(s.state, &auth.DeviceState{
		Brand: "canonical",
		Model: "pc",
	})
	st.Unlock()

	mockKey := keys.EncryptionKey{1, 2, 3, 4}

	var hookCalled []string
	hookInvoke := func(ctx *hookstate.Context, tomb *tomb.Tomb) ([]byte, error) {
		ctx.Lock()
		defer ctx.Unlock()

		// check that the context has the right data
		c.Check(ctx.HookName(), Equals, "fde-setup")
		var fdeSetup fde.SetupRequest
		ctx.Get("fde-setup-request", &fdeSetup)
		c.Check(fdeSetup, DeepEquals, fde.SetupRequest{
			Op:      "op",
			Key:     mockKey[:],
			KeyName: "some-key-name",
		})
		ctx.Set("fde-setup-result", []byte("result"))
		hookCalled = append(hookCalled, ctx.InstanceName())
		return nil, nil
	}

	rhk := hookstate.MockRunHook(hookInvoke)
	defer rhk()

	s.o.Loop()
	defer s.o.Stop()

	req := &fde.SetupRequest{
		Op:      "op",
		Key:     mockKey[:],
		KeyName: "some-key-name",
	}
	st.Lock()
	res, err := devicestate.DeviceManagerRunFDESetupHook(s.mgr, req)
	st.Unlock()
	c.Assert(err, IsNil)
	c.Check(res, DeepEquals, []byte("result"))
	c.Check(hookCalled, DeepEquals, []string{"pc-kernel"})
}

func (s *deviceMgrSuite) TestRunFDESetupHookErrors(c *C) {
	st := s.state

	st.Lock()
	makeInstalledMockKernelSnap(c, st, kernelYamlWithFdeSetup)
	s.makeModelAssertionInState(c, "canonical", "pc", map[string]any{
		"architecture": "amd64",
		"kernel":       "pc-kernel",
		"gadget":       "pc",
	})
	devicestatetest.SetDevice(s.state, &auth.DeviceState{
		Brand: "canonical",
		Model: "pc",
	})
	st.Unlock()

	hookInvoke := func(ctx *hookstate.Context, tomb *tomb.Tomb) ([]byte, error) {
		return nil, fmt.Errorf("hook failed")
	}

	rhk := hookstate.MockRunHook(hookInvoke)
	defer rhk()

	s.o.Loop()
	defer s.o.Stop()

	req := &fde.SetupRequest{
		Op: "op",
	}
	st.Lock()
	_, err := devicestate.DeviceManagerRunFDESetupHook(s.mgr, req)
	st.Unlock()
	c.Assert(err, ErrorMatches, `cannot run hook for "op": run hook "fde-setup": hook failed`)
}

func (s *deviceMgrSuite) TestRunFDESetupHookErrorResult(c *C) {
	st := s.state

	st.Lock()
	makeInstalledMockKernelSnap(c, st, kernelYamlWithFdeSetup)
	s.makeModelAssertionInState(c, "canonical", "pc", map[string]any{
		"architecture": "amd64",
		"kernel":       "pc-kernel",
		"gadget":       "pc",
	})
	devicestatetest.SetDevice(s.state, &auth.DeviceState{
		Brand: "canonical",
		Model: "pc",
	})
	st.Unlock()

	hookInvoke := func(ctx *hookstate.Context, tomb *tomb.Tomb) ([]byte, error) {
		// Simulate an incorrect type for fde-setup-result here to
		// test error string from runFDESetupHook.
		// This should never happen in practice, "snapctl fde-setup"
		// will always set this to []byte
		ctx.Lock()
		ctx.Set("fde-setup-result", "not-bytes")
		ctx.Unlock()
		return nil, nil
	}

	rhk := hookstate.MockRunHook(hookInvoke)
	defer rhk()

	s.o.Loop()
	defer s.o.Stop()

	req := &fde.SetupRequest{
		Op: "op",
	}
	st.Lock()
	_, err := devicestate.DeviceManagerRunFDESetupHook(s.mgr, req)
	st.Unlock()
	c.Assert(err, ErrorMatches, `cannot get result from fde-setup hook "op": cannot unmarshal context value for "fde-setup-result": illegal base64 data at input byte 3`)
}

type startOfOperationTimeSuite struct {
	state  *state.State
	mgr    *devicestate.DeviceManager
	runner *state.TaskRunner
}

var _ = Suite(&startOfOperationTimeSuite{})

func (s *startOfOperationTimeSuite) SetUpTest(c *C) {
	dirs.SetRootDir(c.MkDir())
	os.MkdirAll(dirs.SnapRunDir, 0755)

	s.state = state.New(nil)
	s.runner = state.NewTaskRunner(s.state)
	s.mgr = nil
}

func (s *startOfOperationTimeSuite) TearDownTest(c *C) {
	dirs.SetRootDir("")
}

func (s *startOfOperationTimeSuite) manager(c *C) *devicestate.DeviceManager {
	if s.mgr == nil {
		hookMgr, err := hookstate.Manager(s.state, s.runner)
		c.Assert(err, IsNil)
		mgr, err := devicestate.Manager(s.state, hookMgr, s.runner, nil)
		c.Assert(err, IsNil)
		s.mgr = mgr
	}
	return s.mgr
}

func (s *startOfOperationTimeSuite) TestStartOfOperationTimeFromSeedTime(c *C) {
	mgr := s.manager(c)

	st := s.state
	st.Lock()
	defer st.Unlock()

	seedTime := time.Now().AddDate(0, -1, 0)
	st.Set("seed-time", seedTime)

	operationTime, err := mgr.StartOfOperationTime()
	c.Assert(err, IsNil)
	c.Check(operationTime.Equal(seedTime), Equals, true)

	var op time.Time
	st.Get("start-of-operation-time", &op)
	c.Check(op.Equal(operationTime), Equals, true)
}

func (s *startOfOperationTimeSuite) TestStartOfOperationTimeAlreadySet(c *C) {
	mgr := s.manager(c)

	st := s.state
	st.Lock()
	defer st.Unlock()

	op := time.Now().AddDate(0, -1, 0)
	st.Set("start-of-operation-time", op)

	operationTime, err := mgr.StartOfOperationTime()
	c.Assert(err, IsNil)
	c.Check(operationTime.Equal(op), Equals, true)
}

func (s *startOfOperationTimeSuite) TestStartOfOperationTimeNoSeedTime(c *C) {
	mgr := s.manager(c)

	st := s.state
	st.Lock()
	defer st.Unlock()

	now := time.Now().Add(-1 * time.Second)
	devicestate.MockTimeNow(func() time.Time {
		return now
	})

	operationTime, err := mgr.StartOfOperationTime()
	c.Assert(err, IsNil)
	c.Check(operationTime.Equal(now), Equals, true)

	// repeated call returns already set time
	prev := now
	now = time.Now().Add(-10 * time.Hour)
	operationTime, err = s.manager(c).StartOfOperationTime()
	c.Assert(err, IsNil)
	c.Check(operationTime.Equal(prev), Equals, true)
}

func (s *startOfOperationTimeSuite) TestStartOfOperationErrorIfPreseed(c *C) {
	restore := snapdenv.MockPreseeding(true)
	defer restore()

	mgr := s.manager(c)
	st := s.state

	st.Lock()
	defer st.Unlock()
	_, err := mgr.StartOfOperationTime()
	c.Assert(err, ErrorMatches, `internal error: unexpected call to StartOfOperationTime in preseed mode`)
}

func (s *deviceMgrSuite) TestCreateSeedRefreshTasks(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	restore := devicestate.MockTimeNow(func() time.Time {
		return time.Date(2026, 2, 27, 8, 30, 0, 0, time.UTC)
	})
	defer restore()

	tSnap1 := s.state.NewTask("fake-download", "...")
	tSnap2 := s.state.NewTask("fake-download", "...")
	tComp1 := s.state.NewTask("fake-download-component", "...")
	dctx := s.seedRefreshDeviceContext(c, "snap-1", "snap-2")

	seedTS, added, err := devicestate.SeedRefreshTasks(s.state, dctx, []snapstate.SeedRefreshCandidate{
		{
			InstanceName:          "snap-1",
			SnapSetupTaskIDs:      []string{tSnap1.ID()},
			ComponentSetupTaskIDs: []string{tComp1.ID()},
		},
		{
			InstanceName:     "snap-2",
			SnapSetupTaskIDs: []string{tSnap2.ID()},
		},
	})
	c.Assert(err, IsNil)
	c.Assert(added, DeepEquals, map[string]bool{"snap-1": true, "snap-2": true})

	c.Assert(seedTS.Create.Kind(), Equals, "create-recovery-system")
	c.Assert(seedTS.Finalize.Kind(), Equals, "finalize-recovery-system")
	c.Check(seedTS.Remove, HasLen, 0)

	const expectedLabel = "20260227"

	var systemSetupData map[string]any
	c.Assert(seedTS.Create.Get("recovery-system-setup", &systemSetupData), IsNil)
	c.Assert(systemSetupData, DeepEquals, map[string]any{
		"label":                 expectedLabel,
		"directory":             filepath.Join(boot.InitramfsUbuntuSeedDir, "systems", expectedLabel),
		"snap-setup-tasks":      []any{tSnap1.ID(), tSnap2.ID()},
		"component-setup-tasks": []any{tComp1.ID()},
		"mark-default":          true,
		"seed-refresh":          true,
		"test-system":           true,
	})

	var setupTaskID string
	c.Assert(seedTS.Finalize.Get("recovery-system-setup-task", &setupTaskID), IsNil)
	c.Check(setupTaskID, Equals, seedTS.Create.ID())

	var boundary restart.RestartBoundaryDirection
	c.Assert(seedTS.Create.Get("restart-boundary", &boundary), IsNil)
	c.Check(boundary, Equals, restart.RestartBoundaryDirectionDo)
}

func (s *deviceMgrSuite) TestCreateSeedRefreshTasksUsesNextAvailableLabel(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	restore := devicestate.MockTimeNow(func() time.Time {
		return time.Date(2026, 2, 27, 9, 0, 0, 0, time.UTC)
	})
	defer restore()

	const labelBase = "20260227"
	systemsDir := filepath.Join(boot.InitramfsUbuntuSeedDir, "systems")
	c.Assert(os.MkdirAll(filepath.Join(systemsDir, labelBase), 0755), IsNil)
	c.Assert(os.MkdirAll(filepath.Join(systemsDir, labelBase+"-1"), 0755), IsNil)
	c.Assert(os.MkdirAll(filepath.Join(systemsDir, labelBase+"-2"), 0755), IsNil)

	tSnap := s.state.NewTask("fake-download", "...")
	dctx := s.seedRefreshDeviceContext(c, "snap-1")

	seedTS, added, err := devicestate.SeedRefreshTasks(s.state, dctx, []snapstate.SeedRefreshCandidate{
		{
			InstanceName:     "snap-1",
			SnapSetupTaskIDs: []string{tSnap.ID()},
		},
	})
	c.Assert(err, IsNil)
	c.Assert(added, DeepEquals, map[string]bool{"snap-1": true})

	c.Assert(seedTS.Create.Kind(), Equals, "create-recovery-system")
	c.Assert(seedTS.Finalize.Kind(), Equals, "finalize-recovery-system")
	c.Check(seedTS.Remove, HasLen, 0)

	const expectedLabel = labelBase + "-3"

	var systemSetupData map[string]any
	c.Assert(seedTS.Create.Get("recovery-system-setup", &systemSetupData), IsNil)
	c.Check(systemSetupData["label"], Equals, expectedLabel)
	c.Check(systemSetupData["directory"], Equals, filepath.Join(boot.InitramfsUbuntuSeedDir, "systems", expectedLabel))
	c.Check(systemSetupData["snap-setup-tasks"], DeepEquals, []any{tSnap.ID()})
	c.Check(systemSetupData["mark-default"], Equals, true)
	c.Check(systemSetupData["seed-refresh"], Equals, true)
	c.Check(systemSetupData["test-system"], Equals, true)
}

func (s *deviceMgrSuite) TestCreateSeedRefreshTasksAddsPruneTasks(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	restore := devicestate.MockTimeNow(func() time.Time {
		return time.Date(2026, 2, 27, 9, 0, 0, 0, time.UTC)
	})
	defer restore()

	s.state.Set("seeded-systems", []devicestate.SeededSystem{
		{System: "current-seed-refresh", SeedRefresh: true},
		{System: "non-seed-refresh"},
		{System: "old-seed-refresh-1", SeedRefresh: true},
		{System: "old-seed-refresh-2", SeedRefresh: true},
	})
	dctx := s.seedRefreshDeviceContext(c, "snap-1")
	tSnap := s.state.NewTask("fake-download", "...")

	seedTS, added, err := devicestate.SeedRefreshTasks(s.state, dctx, []snapstate.SeedRefreshCandidate{
		{
			InstanceName:     "snap-1",
			SnapSetupTaskIDs: []string{tSnap.ID()},
		},
	})
	c.Assert(err, IsNil)
	c.Assert(added, DeepEquals, map[string]bool{"snap-1": true})
	c.Assert(seedTS.Remove, HasLen, 2)
	c.Check(seedTS.Remove[0].WaitTasks(), DeepEquals, []*state.Task{seedTS.Finalize})
	c.Check(seedTS.Remove[1].WaitTasks(), DeepEquals, []*state.Task{seedTS.Finalize})

	var labels []string
	for _, remove := range seedTS.Remove {
		var setup map[string]any
		c.Assert(remove.Get("remove-recovery-system-setup", &setup), IsNil)
		labels = append(labels, setup["label"].(string))
	}

	c.Check(labels, testutil.DeepUnsortedMatches, []string{"old-seed-refresh-1", "old-seed-refresh-2"})
}

func (s *deviceMgrSuite) TestUpdateSeedRefreshChange(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	dctx := s.seedRefreshDeviceContext(c, "snap-1", "snap-2")
	chg := s.state.NewChange("seed-refresh", "...")
	snap1Task := s.state.NewTask("fake-download", "...")
	snap2Task := s.state.NewTask("fake-download", "...")
	snap3Task := s.state.NewTask("fake-download", "...")
	create := s.state.NewTask("create-recovery-system", "...")
	create.Set("recovery-system-setup", &devicestate.RecoverySystemSetup{
		Label:               "20260227",
		Directory:           filepath.Join(boot.InitramfsUbuntuSeedDir, "systems", "20260227"),
		SnapSetupTasks:      []string{snap1Task.ID()},
		ComponentSetupTasks: []string{"comp-1"},
	})
	finalize := s.state.NewTask("finalize-recovery-system", "...")
	finalize.Set("recovery-system-setup-task", create.ID())
	remove := s.state.NewTask("remove-recovery-system", "...")
	remove.WaitFor(finalize)
	chg.AddTask(create)
	chg.AddTask(finalize)
	chg.AddTask(remove)

	seedTS, err := devicestate.UpdateSeedRefreshChange(chg, dctx, snapstate.SeedRefreshCandidate{
		InstanceName:          "snap-2",
		SnapSetupTaskIDs:      []string{snap2Task.ID()},
		ComponentSetupTaskIDs: []string{"comp-2", "comp-1"},
	})
	c.Assert(err, IsNil)
	c.Assert(seedTS, NotNil)
	c.Check(seedTS.Create, Equals, create)
	c.Check(seedTS.Finalize, Equals, finalize)
	c.Check(seedTS.Remove, DeepEquals, []*state.Task{remove})

	seedTS, err = devicestate.UpdateSeedRefreshChange(chg, dctx, snapstate.SeedRefreshCandidate{
		InstanceName:          "snap-1",
		SnapSetupTaskIDs:      []string{snap1Task.ID()},
		ComponentSetupTaskIDs: []string{"comp-3"},
	})
	c.Assert(err, IsNil)
	c.Assert(seedTS, NotNil)

	seedTS, err = devicestate.UpdateSeedRefreshChange(chg, dctx, snapstate.SeedRefreshCandidate{
		InstanceName:          "snap-3",
		SnapSetupTaskIDs:      []string{snap3Task.ID()},
		ComponentSetupTaskIDs: []string{"comp-4"},
	})
	c.Assert(err, IsNil)
	c.Check(seedTS, IsNil)

	var setup devicestate.RecoverySystemSetup
	c.Assert(create.Get("recovery-system-setup", &setup), IsNil)
	c.Check(setup.SnapSetupTasks, DeepEquals, []string{snap1Task.ID(), snap2Task.ID()})
	c.Check(setup.ComponentSetupTasks, DeepEquals, []string{"comp-1", "comp-2", "comp-3"})
}

func (s *deviceMgrSuite) TestUpdateSeedRefreshChangeUsesPendingSeedRefreshTasks(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	dctx := s.seedRefreshDeviceContext(c, "snap-1", "snap-2")
	chg := s.state.NewChange("seed-refresh", "...")

	oldSnapTask := s.state.NewTask("fake-download", "...")
	oldCreate := s.state.NewTask("create-recovery-system", "...")
	oldCreate.Set("recovery-system-setup", &devicestate.RecoverySystemSetup{
		Label:          "20260226",
		Directory:      filepath.Join(boot.InitramfsUbuntuSeedDir, "systems", "20260226"),
		SnapSetupTasks: []string{oldSnapTask.ID()},
	})
	oldFinalize := s.state.NewTask("finalize-recovery-system", "...")
	oldFinalize.Set("recovery-system-setup-task", oldCreate.ID())
	oldRemove := s.state.NewTask("remove-recovery-system", "...")
	oldRemove.WaitFor(oldFinalize)
	oldCreate.SetStatus(state.DoneStatus)
	oldFinalize.SetStatus(state.DoneStatus)
	oldRemove.SetStatus(state.DoneStatus)

	currentSnapTask := s.state.NewTask("fake-download", "...")
	currentCreate := s.state.NewTask("create-recovery-system", "...")
	currentCreate.Set("recovery-system-setup", &devicestate.RecoverySystemSetup{
		Label:          "20260227",
		Directory:      filepath.Join(boot.InitramfsUbuntuSeedDir, "systems", "20260227"),
		SnapSetupTasks: []string{currentSnapTask.ID()},
	})
	currentFinalize := s.state.NewTask("finalize-recovery-system", "...")
	currentFinalize.Set("recovery-system-setup-task", currentCreate.ID())
	currentRemove := s.state.NewTask("remove-recovery-system", "...")
	currentRemove.WaitFor(currentFinalize)

	chg.AddTask(oldCreate)
	chg.AddTask(oldFinalize)
	chg.AddTask(oldRemove)
	chg.AddTask(currentCreate)
	chg.AddTask(currentFinalize)
	chg.AddTask(currentRemove)

	nextSnapTask := s.state.NewTask("fake-download", "...")
	seedTS, err := devicestate.UpdateSeedRefreshChange(chg, dctx, snapstate.SeedRefreshCandidate{
		InstanceName:     "snap-2",
		SnapSetupTaskIDs: []string{nextSnapTask.ID()},
	})
	c.Assert(err, IsNil)
	c.Assert(seedTS, NotNil)
	c.Check(seedTS.Create, Equals, currentCreate)
	c.Check(seedTS.Finalize, Equals, currentFinalize)
	c.Check(seedTS.Remove, DeepEquals, []*state.Task{currentRemove})

	var oldSetup devicestate.RecoverySystemSetup
	c.Assert(oldCreate.Get("recovery-system-setup", &oldSetup), IsNil)
	c.Check(oldSetup.SnapSetupTasks, DeepEquals, []string{oldSnapTask.ID()})

	var currentSetup devicestate.RecoverySystemSetup
	c.Assert(currentCreate.Get("recovery-system-setup", &currentSetup), IsNil)
	c.Check(currentSetup.SnapSetupTasks, DeepEquals, []string{currentSnapTask.ID(), nextSnapTask.ID()})
}

func (s *deviceMgrSuite) seedRefreshDeviceContext(c *C, requiredSnaps ...string) snapstate.DeviceContext {
	extras := map[string]any{
		"architecture": "amd64",
		"kernel":       "pc-kernel",
		"gadget":       "pc",
	}
	if len(requiredSnaps) != 0 {
		required := make([]any, len(requiredSnaps))
		for i, snapName := range requiredSnaps {
			required[i] = snapName
		}
		extras["required-snaps"] = required
	}

	model := s.makeModelAssertionInState(c, "canonical", "pc", extras)
	return &snapstatetest.TrivialDeviceContext{DeviceModel: model}
}

func (s *deviceMgrSuite) TestRemoveRecoverySystemBlockedWhenAnotherRemovalRunning(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	other := s.state.NewTask("remove-recovery-system", "remove one")
	otherRemove := s.state.NewTask("remove-recovery-system", "remove two")
	otherRemove.SetStatus(state.DoingStatus)

	c.Assert(devicestate.RemoveRecoverySystemBlocked(other, []*state.Task{otherRemove}), Equals, true)
}

func (s *deviceMgrSuite) TestRemoveRecoverySystemDoesNotBlockOtherTasks(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	remove := s.state.NewTask("remove-recovery-system", "remove one")
	other := s.state.NewTask("other-task", "other")
	other.SetStatus(state.DoingStatus)

	c.Assert(devicestate.RemoveRecoverySystemBlocked(remove, []*state.Task{other}), Equals, false)
	c.Assert(devicestate.RemoveRecoverySystemBlocked(other, []*state.Task{remove}), Equals, false)
}

func (s *deviceMgrSuite) TestCanAutoRefreshNTP(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	// CanAutoRefresh is ready
	s.state.Set("seeded", true)
	s.makeModelAssertionInState(c, "canonical", "pc", map[string]any{
		"architecture": "amd64",
		"kernel":       "pc-kernel",
		"gadget":       "pc",
	})
	devicestatetest.SetDevice(s.state, &auth.DeviceState{
		Brand:  "canonical",
		Model:  "pc",
		Serial: "8989",
	})
	s.makeSerialAssertionInState(c, "canonical", "pc", "8989")

	// now check that the ntp-sync information is honored
	n := 0
	ntpSynced := false
	restore := devicestate.MockTimeutilIsNTPSynchronized(func() (bool, error) {
		n++
		return ntpSynced, nil
	})
	defer restore()

	// not ntp-synced
	ok, err := devicestate.CanAutoRefresh(s.state)
	c.Assert(err, IsNil)
	c.Check(ok, Equals, false)
	c.Check(n, Equals, 1)

	// now ntp-synced
	ntpSynced = true
	ok, err = devicestate.CanAutoRefresh(s.state)
	c.Assert(err, IsNil)
	c.Check(ok, Equals, true)
	c.Check(n, Equals, 2)

	// and the result was cached
	ok, err = devicestate.CanAutoRefresh(s.state)
	c.Assert(err, IsNil)
	c.Check(ok, Equals, true)
	c.Check(n, Equals, 2)
}

func (s *deviceMgrSuite) TestNTPSyncedOrWaitedLongerThan(c *C) {
	restore := devicestate.MockTimeutilIsNTPSynchronized(func() (bool, error) {
		return false, nil
	})
	defer restore()

	// NTP is not synced yet and the (arbitrary selected) wait
	// time of 12h since the device manager got started is not
	// over yet
	syncedOrWaited := devicestate.DeviceManagerNTPSyncedOrWaitedLongerThan(s.mgr, 12*time.Hour)
	c.Check(syncedOrWaited, Equals, false)

	// NTP is also not synced here but the wait time of 1
	// Nanosecond since the device manager got started is
	// certainly over
	syncedOrWaited = devicestate.DeviceManagerNTPSyncedOrWaitedLongerThan(s.mgr, 1*time.Nanosecond)
	c.Check(syncedOrWaited, Equals, true)
}

func (s *deviceMgrSuite) TestNTPSyncedOrWaitedNoTimedate1(c *C) {
	n := 0
	restore := devicestate.MockTimeutilIsNTPSynchronized(func() (bool, error) {
		n++
		// no timedate1
		return false, timeutil.NoTimedate1Error{Err: fmt.Errorf("boom")}
	})
	defer restore()

	// There is no timedate1 dbus service, no point in waiting
	syncedOrWaited := devicestate.DeviceManagerNTPSyncedOrWaitedLongerThan(s.mgr, 12*time.Hour)
	c.Check(syncedOrWaited, Equals, true)
	c.Check(n, Equals, 1)

	// and the result was cached
	syncedOrWaited = devicestate.DeviceManagerNTPSyncedOrWaitedLongerThan(s.mgr, 12*time.Hour)
	c.Check(syncedOrWaited, Equals, true)
	c.Check(n, Equals, 1)
}

func (s *deviceMgrSuite) TestVoidDirPermissionsGetFixed(c *C) {
	// create /var/lib/snapd/void with the wrong permissions
	err := os.MkdirAll(dirs.SnapVoidDir, 0755)
	c.Assert(err, IsNil)

	logbuf, restore := logger.MockLogger()
	defer restore()

	mgr, err := devicestate.Manager(s.state, s.hookMgr, s.o.TaskRunner(), s.newStore)
	c.Assert(err, IsNil)
	err = mgr.StartUp()
	c.Assert(err, IsNil)

	st, err := os.Stat(dirs.SnapVoidDir)
	c.Assert(err, IsNil)
	c.Check(int(st.Mode().Perm()), Equals, 0111)
	msgs := strings.TrimSpace(logbuf.String())
	c.Check(msgs, Matches, "(?sm).*fixing permissions of .*/var/lib/snapd/void to 0111")
	c.Check(strings.Split(msgs, "\n"), HasLen, 1)
}

func (s *deviceMgrSuite) TestDeviceManagerEnsurePostFactoryResetEncrypted(c *C) {
	defer release.MockOnClassic(false)

	s.state.Lock()
	s.state.Set("seeded", true)
	s.state.Unlock()
	restore := devicestate.SetBootOkRanForCurrentBootID(s.mgr, false)
	defer restore()
	devicestate.SetSystemMode(s.mgr, "run")
	devicestate.SetEarlyBootLocaleConfigUpdatedRan(s.mgr, true)

	// encrypted system
	mockSnapFDEFile(c, "marker", nil)
	err := os.WriteFile(filepath.Join(dirs.SnapFDEDir, "ubuntu-save.key"),
		[]byte("save-key"), 0644)
	c.Assert(err, IsNil)
	c.Assert(os.MkdirAll(boot.InitramfsSeedEncryptionKeyDir, 0755), IsNil)
	err = os.WriteFile(filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-save.recovery.sealed-key"),
		[]byte("old"), 0644)
	c.Assert(err, IsNil)
	err = os.WriteFile(filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-save.recovery.sealed-key.factory-reset"),
		[]byte("save"), 0644)
	c.Assert(err, IsNil)
	// matches the .factory key
	factoryResetMarkercontent := []byte(`{"fallback-save-key-sha3-384":"d192153f0a50e826c6eb400c8711750ed0466571df1d151aaecc8c73095da7ec104318e7bf74d5e5ae2940827bf8402b"}
`)
	c.Assert(os.WriteFile(filepath.Join(dirs.SnapDeviceDir, "factory-reset"), factoryResetMarkercontent, 0644), IsNil)

	transitionCalls := 0
	restore = devicestate.MockSecbootTransitionEncryptionKeyChange(func(mountpoint string, key keys.EncryptionKey) error {
		transitionCalls++
		return nil
	})
	defer restore()

	restore = devicestate.MockDisksDMCryptUUIDFromMountPoint(func(mountpoint string) (string, error) {
		c.Check(mountpoint, Equals, boot.InitramfsUbuntuSaveDir)
		return "FOOUUID", nil
	})
	defer restore()

	removedOldHandles := 0
	restore = devicestate.MockSecbootRemoveOldCounterHandles(func(node string, possibleOldKeys map[string]bool, possibleKeyFiles []string, hintExpectFDEHook bool) error {
		removedOldHandles++
		c.Check(hintExpectFDEHook, Equals, false)
		c.Check(node, Equals, "/dev/disk/by-uuid/FOOUUID")
		c.Check(possibleOldKeys, DeepEquals, map[string]bool{
			"reprovision-default-fallback": true,
		})
		switch removedOldHandles {
		case 1:
			c.Check(possibleKeyFiles, DeepEquals, []string{
				filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-save.recovery.sealed-key"),
			})
		case 2:
			c.Check(possibleKeyFiles, IsNil)
		default:
			c.Errorf("unexpected call")
			return fmt.Errorf("unexpected call")
		}
		return nil
	})
	defer restore()

	deleteOldSaveKey := 0
	restore = devicestate.MockSecbootDeleteKeys(func(node string, matches map[string]bool) error {
		c.Check(node, Equals, "/dev/disk/by-uuid/FOOUUID")
		c.Check(matches, DeepEquals, map[string]bool{
			"reprovision-default":          true,
			"reprovision-default-fallback": true,
		})
		deleteOldSaveKey++
		return nil
	})
	defer restore()

	removeOldDiskKeys := 0
	restore = devicestate.MockSecbootDeleteOldKeys(func(devicePath string) error {
		c.Check(devicePath, Equals, "/dev/disk/by-uuid/FOOUUID")
		removeOldDiskKeys++
		return nil
	})
	defer restore()

	err = s.mgr.Ensure()
	c.Assert(err, IsNil)
	c.Check(transitionCalls, Equals, 0)
	c.Check(deleteOldSaveKey, Equals, 1)
	c.Check(removeOldDiskKeys, Equals, 1)
	// factory reset marker is gone, the key was verified successfully
	c.Check(filepath.Join(dirs.SnapDeviceDir, "factory-reset"), testutil.FileAbsent)
	c.Check(filepath.Join(dirs.SnapFDEDir, "marker"), testutil.FilePresent)
	c.Check(filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-save.recovery.sealed-key.factory-reset"), testutil.FileAbsent)

	transitionCalls = 0
	deleteOldSaveKey = 0
	removeOldDiskKeys = 0
	// try again, no marker, nothing should happen
	devicestate.SetPostFactoryResetRan(s.mgr, false)
	err = s.mgr.Ensure()
	c.Assert(err, IsNil)
	// nothing was called
	c.Check(transitionCalls, Equals, 0)
	c.Check(deleteOldSaveKey, Equals, 0)
	c.Check(removedOldHandles, Equals, 1)
	c.Check(removeOldDiskKeys, Equals, 0)

	// have the marker, but keep the keys rotated as if boot code would do it and
	// try again, in this setup the marker hash matches the migrated key
	c.Assert(os.WriteFile(filepath.Join(dirs.SnapDeviceDir, "factory-reset"), factoryResetMarkercontent, 0644), IsNil)

	devicestate.SetPostFactoryResetRan(s.mgr, false)
	err = s.mgr.Ensure()
	c.Assert(err, IsNil)
	c.Check(transitionCalls, Equals, 0)
	c.Check(deleteOldSaveKey, Equals, 1)
	c.Check(removedOldHandles, Equals, 2)
	c.Check(removeOldDiskKeys, Equals, 1)
	// the marker was again removed
	c.Check(filepath.Join(dirs.SnapDeviceDir, "factory-reset"), testutil.FileAbsent)
}

func (s *deviceMgrSuite) TestDeviceManagerEnsurePostFactoryResetEncryptedError(c *C) {
	defer release.MockOnClassic(false)

	s.state.Lock()
	s.state.Set("seeded", true)
	s.state.Unlock()
	restore := devicestate.SetBootOkRanForCurrentBootID(s.mgr, false)
	defer restore()
	devicestate.SetSystemMode(s.mgr, "run")
	devicestate.SetEarlyBootLocaleConfigUpdatedRan(s.mgr, true)

	// encrypted system
	mockSnapFDEFile(c, "marker", nil)
	c.Assert(os.MkdirAll(boot.InitramfsSeedEncryptionKeyDir, 0755), IsNil)
	err := os.WriteFile(filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-save.recovery.sealed-key"),
		[]byte("old"), 0644)
	c.Check(err, IsNil)
	err = os.WriteFile(filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-save.recovery.sealed-key.factory-reset"),
		[]byte("save"), 0644)
	c.Check(err, IsNil)
	// does not match the save key
	factoryResetMarkercontent := []byte(`{"fallback-save-key-sha3-384":"uh-oh"}
`)
	c.Assert(os.WriteFile(filepath.Join(dirs.SnapDeviceDir, "factory-reset"), factoryResetMarkercontent, 0644), IsNil)

	err = s.mgr.Ensure()
	c.Assert(err, ErrorMatches, "devicemgr: cannot verify factory reset marker: fallback sealed key digest mismatch, got d192153f0a50e826c6eb400c8711750ed0466571df1d151aaecc8c73095da7ec104318e7bf74d5e5ae2940827bf8402b expected uh-oh")

	// factory reset marker is gone, the key was verified successfully
	c.Check(filepath.Join(dirs.SnapDeviceDir, "factory-reset"), testutil.FilePresent)
	c.Check(filepath.Join(dirs.SnapFDEDir, "marker"), testutil.FilePresent)

	// try again, no marker, hit the same error
	devicestate.SetPostFactoryResetRan(s.mgr, false)
	err = s.mgr.Ensure()
	c.Assert(err, ErrorMatches, "devicemgr: cannot verify factory reset marker: fallback sealed key digest mismatch, got d192153f0a50e826c6eb400c8711750ed0466571df1d151aaecc8c73095da7ec104318e7bf74d5e5ae2940827bf8402b expected uh-oh")

	// and again, but not resetting the 'ran' check, so nothing is checked or called
	err = s.mgr.Ensure()
	c.Assert(err, IsNil)
}

func (s *deviceMgrSuite) TestDeviceManagerEnsurePostFactoryResetUnencrypted(c *C) {
	defer release.MockOnClassic(false)

	s.state.Lock()
	s.state.Set("seeded", true)
	s.state.Unlock()
	restore := devicestate.SetBootOkRanForCurrentBootID(s.mgr, false)
	defer restore()
	devicestate.SetSystemMode(s.mgr, "run")
	devicestate.SetEarlyBootLocaleConfigUpdatedRan(s.mgr, true)

	// mock the factory reset marker of a system that isn't encrypted
	c.Assert(os.MkdirAll(dirs.SnapDeviceDir, 0755), IsNil)
	c.Assert(os.WriteFile(filepath.Join(dirs.SnapDeviceDir, "factory-reset"), []byte("{}"), 0644), IsNil)

	err := s.mgr.Ensure()
	c.Assert(err, IsNil)

	// factory reset marker is gone
	c.Check(filepath.Join(dirs.SnapDeviceDir, "factory-reset"), testutil.FileAbsent)

	// try again, no marker, nothing should happen
	devicestate.SetPostFactoryResetRan(s.mgr, false)
	err = s.mgr.Ensure()
	c.Assert(err, IsNil)
}

func (s *deviceMgrSuite) mockSystemUser(c *C, username string, expiration time.Time) {
	_, err := auth.NewUser(s.state, auth.NewUserParams{
		Username:   username,
		Email:      "email1@test.com",
		Macaroon:   "macaroon",
		Discharges: []string{"discharge"},
		Expiration: expiration,
	})
	c.Assert(err, IsNil)
}

func (s *deviceMgrSuite) mockSystemMode(c *C, mode string) {
	modeEnv := &boot.Modeenv{Mode: mode}
	err := modeEnv.WriteTo("")
	c.Assert(err, IsNil)
	devicestate.SetSystemMode(s.mgr, mode)
}

func (s *deviceMgrSuite) testExpiredUserRemoved(c *C, userToRemove string, extraUsers bool) {
	// Mock the delete user callback to verify it's correctly called. On ubuntu core
	// systems ExtraUsers should be set, where on classic systems ExtraUsers should not
	// be set
	var delUserCalled bool
	r := devicestate.MockOsutilDelUser(func(name string, opts *osutil.DelUserOptions) error {
		delUserCalled = true
		c.Check(name, Equals, userToRemove)
		c.Check(opts, NotNil)
		c.Check(opts.ExtraUsers, Equals, extraUsers)
		return nil
	})
	defer r()

	s.state.Unlock()
	err := devicestate.EnsureExpiredUsersRemoved(s.mgr)
	c.Assert(err, IsNil)
	c.Assert(delUserCalled, Equals, true)
}

func (s *deviceMgrSuite) testExpiredUserNotRemoved(c *C) {
	// Mock the delete user callback to verify it's correctly called. On ubuntu core
	// systems ExtraUsers should be set, where on classic systems ExtraUsers should not
	// be set
	var delUserCalled bool
	r := devicestate.MockOsutilDelUser(func(name string, opts *osutil.DelUserOptions) error {
		delUserCalled = true
		return nil
	})
	defer r()

	s.state.Unlock()
	err := devicestate.EnsureExpiredUsersRemoved(s.mgr)
	c.Assert(err, IsNil)
	c.Assert(delUserCalled, Equals, false)
}

func (s *deviceMgrSuite) TestEnsureExpiredUsersRemovedOnCore(c *C) {
	s.mockSystemMode(c, "run")
	s.state.Lock()

	// Set seeded otherwise we won't get very far in this test, as we don't
	// remove users when not seeded.
	s.state.Set("seeded", true)

	// We mock a few users, with one of them having expired. We then run
	// the code to see that it correctly removes it from the system. Important
	// to also test with a user that has no expiration to make sure the previous
	// users are unaffected.
	s.mockSystemUser(c, "user1", time.Time{})
	s.mockSystemUser(c, "expires-soon", time.Now().Add(time.Minute*5))
	s.mockSystemUser(c, "remove-me", time.Now().Add(-(time.Minute * 5)))
	s.testExpiredUserRemoved(c, "remove-me", true)
}

func (s *deviceMgrSuite) TestEnsureExpiredUsersRemovedOnClassic(c *C) {
	// Mock being on classic, then the EnsureExpiredUsersRemoved should be a no-op
	r := release.MockOnClassic(true)
	defer r()
	s.state.Lock()

	// It's not really needed to set seeded here as the check comes after
	// we check for classic - however in the future, if someone were to reorder
	// checks this would still pass if we didn't set this
	s.state.Set("seeded", true)

	// We mock a few users, with one of them having expired. We then run
	// the code to see that it correctly removes it from the system. Important
	// to also test with a user that has no expiration to make sure the previous
	// users are unaffected.
	s.mockSystemUser(c, "user1", time.Time{})
	s.mockSystemUser(c, "expires-soon", time.Now().Add(time.Minute*5))
	s.mockSystemUser(c, "remove-me", time.Now().Add(-(time.Minute * 5)))
	s.testExpiredUserRemoved(c, "remove-me", false)
}

func (s *deviceMgrSuite) TestEnsureExpiredUsersRemovedNotRecoverMode(c *C) {
	// mock recovery mode
	s.mockSystemMode(c, "recover")

	s.state.Lock()

	// It's not really needed to set seeded here as the check comes after
	// we check for classic - however in the future, if someone were to reorder
	// checks this would still pass if we didn't set this
	s.state.Set("seeded", true)

	// Mock a user that would be expired
	s.mockSystemUser(c, "remove-me", time.Now().Add(-(time.Minute * 5)))
	s.testExpiredUserNotRemoved(c)
}

func (s *deviceMgrSuite) TestEnsureExpiredUsersRemovedNotInstallMode(c *C) {
	// mock install mode
	s.mockSystemMode(c, "install")

	s.state.Lock()

	// It's not really needed to set seeded here as the check comes after
	// we check for classic - however in the future, if someone were to reorder
	// checks this would still pass if we didn't set this
	s.state.Set("seeded", true)

	// Mock a user that would be expired, but expect it not to be removed
	s.mockSystemUser(c, "remove-me", time.Now().Add(-(time.Minute * 5)))
	s.testExpiredUserNotRemoved(c)
}

func (s *deviceMgrSuite) TestEnsureExpiredUsersRemovedNotUnseeded(c *C) {
	s.mockSystemMode(c, "run")
	s.state.Lock()

	// The default seems to be false, but lets be explicit about setting
	// this to false.
	s.state.Set("seeded", false)

	// Mock a user that would be expired, but expect it not to be removed
	s.mockSystemUser(c, "remove-me", time.Now().Add(-(time.Minute * 5)))
	s.testExpiredUserNotRemoved(c)
}

func (s *deviceMgrSuite) TestEnsureEarlyBootXKBConfigUpdatedOnHybrid(c *C) {
	// Mock Resolute hybrid system.
	restore := release.MockReleaseInfo(&release.OS{
		ID:        "ubuntu",
		VersionID: "26.04",
	})
	defer restore()
	s.setHybridModelInState(c)
	s.mockSystemMode(c, "run")

	s.state.Lock()
	s.state.Set("seeded", true)
	s.state.Unlock()

	mockConf := keyboard.XKBConfig{
		Model:    "pc105",
		Variants: nil,
		Layouts:  []string{"uk"},
		Options:  nil,
	}

	called := 0
	restore = devicestate.MockKeyboardCurrentXKBConfig(func() (*keyboard.XKBConfig, error) {
		called++
		return &mockConf, nil
	})
	defer restore()

	var ensureCallback func(config *keyboard.XKBConfig)
	restore = devicestate.MockKeyboardNewXKBConfigListener(func(ctx context.Context, cb func(config *keyboard.XKBConfig)) (*keyboard.XKBConfigListener, error) {
		ensureCallback = cb
		return nil, nil
	})
	defer restore()

	for i := 0; i < 10; i++ {
		err := devicestate.EnsureEarlyBootXKBConfigUpdated(s.mgr)
		c.Assert(err, IsNil)
	}

	// Ensure method only runs once to initialize the XKB
	// config and setup the XKB listener.
	c.Assert(called, Equals, 1)

	s.state.Lock()
	checkExtraSnapdFragments(c, s.state, map[string]string{"xkb": `snapd.xkb="uk,pc105,,"`})
	checkPendingExtraSnapdFragments(c, s.state, true)
	s.state.Set("kcmdline-pending-extra-snapd-fragments", false)
	s.state.Unlock()

	mockConf.Model = "pc104"
	mockConf.Layouts = []string{"us"}
	ensureCallback(&mockConf)
	s.state.Lock()
	checkExtraSnapdFragments(c, s.state, map[string]string{"xkb": `snapd.xkb="us,pc104,,"`})
	checkPendingExtraSnapdFragments(c, s.state, true)
	s.state.Set("kcmdline-pending-extra-snapd-fragments", false)
	s.state.Unlock()

	// Run one more time with args unchanged, nothing should be logged
	ensureCallback(&mockConf)
	s.state.Lock()
	checkExtraSnapdFragments(c, s.state, map[string]string{"xkb": `snapd.xkb="us,pc104,,"`})
	checkPendingExtraSnapdFragments(c, s.state, false)
	s.state.Set("kcmdline-pending-extra-snapd-fragments", false)
	s.state.Unlock()

	// Double check that initialization ran once.
	c.Assert(called, Equals, 1)
}

func (s *deviceMgrSuite) TestEnsureExtraSnapdKernelCommandLineFragmentsApplied(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	s.state.Set("seeded", true)

	checkPendingExtraSnapdFragments(c, s.state, false)
	c.Check(s.state.Changes(), HasLen, 0)

	logbuf, restore := logger.MockLogger()
	defer restore()

	c.Check(logbuf.String(), Equals, "")

	s.state.Unlock()
	err := devicestate.EnsureExtraSnapdKernelCommandLineFragmentsApplied(s.mgr)
	s.state.Lock()
	c.Assert(err, IsNil)
	// no pending fragments, no changes created
	checkPendingExtraSnapdFragments(c, s.state, false)
	c.Check(s.state.Changes(), HasLen, 0)
	c.Check(logbuf.String(), Equals, "")

	// now set pending fragments that need to be applied
	err = devicestate.SetExtraSnapdKernelCommandLineFragment(s.state, "xkb", "some fragment")
	c.Assert(err, IsNil)
	checkPendingExtraSnapdFragments(c, s.state, true)
	c.Check(s.state.Changes(), HasLen, 0)
	c.Check(logbuf.String(), Equals, "")

	s.state.Unlock()
	err = devicestate.EnsureExtraSnapdKernelCommandLineFragmentsApplied(s.mgr)
	s.state.Lock()
	c.Assert(err, IsNil)

	// no actual task ran to unset pending state
	checkPendingExtraSnapdFragments(c, s.state, true)

	c.Check(s.state.Changes(), HasLen, 1)
	chg := s.state.Changes()[0]
	c.Check(chg.Tasks(), HasLen, 1)
	t := chg.Tasks()[0]
	c.Check(t.Kind(), Equals, "update-gadget-cmdline")
	var fromExtraSnapdFragment bool
	err = t.Get("from-extra-snapd-fragments", &fromExtraSnapdFragment)
	c.Assert(err, IsNil)
	c.Check(fromExtraSnapdFragment, Equals, true)
	c.Check(logbuf.String(), testutil.Contains, "applying pending extra snapd kernel cmdline fragments")
}

func (s *deviceMgrSuite) TestEnsureExtraSnapdKernelCommandLineFragmentsAppliedConflictCheck(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	s.state.Set("seeded", true)

	err := devicestate.SetExtraSnapdKernelCommandLineFragment(s.state, "xkb", "some fragment")
	c.Assert(err, IsNil)

	chg := s.state.NewChange("remodel", "...")
	chg.SetStatus(state.DoingStatus)

	logbuf, restore := logger.MockLogger()
	defer restore()

	c.Check(logbuf.String(), Equals, "")

	s.state.Unlock()
	err = devicestate.EnsureExtraSnapdKernelCommandLineFragmentsApplied(s.mgr)
	s.state.Lock()
	c.Assert(err, IsNil)
	c.Check(logbuf.String(), testutil.Contains, "cannot apply extra snapd kernel command line fragments: remodeling in progress, no other changes allowed until this is done")
	// no new change created
	c.Assert(s.state.Changes(), HasLen, 1)

	chg.SetStatus(state.DoneStatus)

	chg = s.state.NewChange("apply-extra-snapd-kcmdline-fragments", "...")
	chg.SetStatus(state.DoingStatus)
	c.Assert(s.state.Changes(), HasLen, 2)

	s.state.Unlock()
	err = devicestate.EnsureExtraSnapdKernelCommandLineFragmentsApplied(s.mgr)
	s.state.Lock()
	c.Assert(err, IsNil)
	// no new change created
	c.Assert(s.state.Changes(), HasLen, 2)
}

func (s *deviceMgrSuite) cacheDeviceCore20Seed(c *C) {

	// now create a minimal uc20 seed dir with snaps/assertions
	ss := &seedtest.SeedSnaps{
		StoreSigning: s.storeSigning,
		Brands:       s.brands,
	}

	// now create a minimal uc20 seed dir with snaps/assertions
	seed20 := &seedtest.TestingSeed20{
		SeedSnaps: *ss,
		SeedDir:   dirs.SnapSeedDir,
	}

	gadgetYaml := `
volumes:
    volume-id:
        bootloader: grub
        structure:
        - name: ubuntu-seed
          role: system-seed
          type: EF,C12A7328-F81F-11D2-BA4B-00A0C93EC93B
          size: 1G
        - name: ubuntu-data
          role: system-data
          type: 83,0FC63DAF-8483-4772-8E79-3D69D8477DE4
          size: 2G
`
	makeSnap := func(yamlKey string) {
		var files [][]string
		if yamlKey == "pc=20" {
			files = append(files, []string{"meta/gadget.yaml", gadgetYaml})
		}
		seed20.MakeAssertedSnap(c, seedtest.SampleSnapYaml[yamlKey], files, snap.R(1), "canonical", seed20.StoreSigning.Database)
	}

	makeSnap("snapd")
	makeSnap("pc-kernel=20")
	makeSnap("core20")
	makeSnap("pc=20")
	optSnapPath := snaptest.MakeTestSnapWithFiles(c, seedtest.SampleSnapYaml["optional20-a"], nil)

	model := map[string]any{
		"display-name": "my model",
		"architecture": "amd64",
		"base":         "core20",
		"grade":        "dangerous",
		"snaps": []any{
			map[string]any{
				"name":            "pc-kernel",
				"id":              seed20.AssertedSnapID("pc-kernel"),
				"type":            "kernel",
				"default-channel": "20",
			},
			map[string]any{
				"name":            "pc",
				"id":              seed20.AssertedSnapID("pc"),
				"type":            "gadget",
				"default-channel": "20",
			},
			map[string]any{
				"name": "snapd",
				"id":   seed20.AssertedSnapID("snapd"),
				"type": "snapd",
			},
			map[string]any{
				"name": "core20",
				"id":   seed20.AssertedSnapID("core20"),
				"type": "base",
			}},
	}

	modelAs := seed20.MakeSeed(c, "20220401", "my-brand", "my-model", model, []*seedwriter.OptionsSnap{{Path: optSnapPath}})
	c.Assert(modelAs, NotNil)

	// mock /he modeenv file
	m := boot.Modeenv{
		Mode:           "install",
		RecoverySystem: "20220401",
	}
	err := m.WriteTo("")
	c.Assert(err, IsNil)

	// reload device seed
	_, _, err = devicestate.ReloadEarlyDeviceSeed(s.mgr, state.ErrNoState)
	c.Assert(err, IsNil)
}

func (s *deviceMgrSuite) TestHandleAutoImportAssertionClassic(c *C) {
	a := devicestate.MockProcessAutoImportAssertion(func(*state.State, seed.Seed, asserts.RODatabase, func(batch *asserts.Batch) error) error {
		panic("trying to process auto-import-assertion on classic system")
	})
	defer a()

	release.OnClassic = true
	s.mockSystemMode(c, "run")

	s.state.Lock()
	defer s.state.Unlock()

	err := devicestate.EnsureAutoImportAssertions(s.mgr)
	c.Check(err, IsNil)

	// ensure state has not been changed
	var autoImported bool
	err = s.state.Get("asserts-early-auto-imported", &autoImported)
	c.Assert(err, testutil.ErrorIs, state.ErrNoState)
	c.Assert(autoImported, Equals, false)
}

func (s *deviceMgrSuite) testHandleAutoImportAssertionInstallModes(c *C, mode string) {
	a := devicestate.MockProcessAutoImportAssertion(func(*state.State, seed.Seed, asserts.RODatabase, func(batch *asserts.Batch) error) error {
		panic("trying to process auto-import-assertion in install modes")
	})
	defer a()

	s.mockSystemMode(c, mode)
	s.state.Lock()
	s.cacheDeviceCore20Seed(c)
	s.state.Set("seeded", nil)
	s.state.Unlock()

	err := devicestate.EnsureAutoImportAssertions(s.mgr)
	c.Check(err, IsNil)

	s.state.Lock()
	defer s.state.Unlock()

	// ensure state has not been changed
	var autoImported bool
	err = s.state.Get("asserts-early-auto-imported", &autoImported)
	c.Assert(err, testutil.ErrorIs, state.ErrNoState)
	c.Assert(autoImported, Equals, false)
}

func (s *deviceMgrSuite) TestHandleAutoImportAssertionInstallMode(c *C) {
	s.testHandleAutoImportAssertionInstallModes(c, "install")
}

func (s *deviceMgrSuite) TestHandleAutoImportAssertionFactoryResetMode(c *C) {
	s.testHandleAutoImportAssertionInstallModes(c, "factory-reset")
}

func (s *deviceMgrSuite) TestHandleAutoImportAssertionWhenDone(c *C) {
	a := devicestate.MockProcessAutoImportAssertion(func(*state.State, seed.Seed, asserts.RODatabase, func(batch *asserts.Batch) error) error {
		panic("trying to process auto-import-assertion after it was already processed")
	})
	defer a()

	s.mockSystemMode(c, "run")

	// set state as processed
	s.state.Lock()
	s.state.Set("asserts-early-auto-imported", true)
	s.cacheDeviceCore20Seed(c)
	s.seeding()
	s.state.Unlock()

	err := devicestate.EnsureAutoImportAssertions(s.mgr)
	c.Check(err, IsNil)

	// check state has not changed
	s.state.Lock()
	defer s.state.Unlock()
	var autoImported bool
	err = s.state.Get("asserts-early-auto-imported", &autoImported)
	c.Assert(err, IsNil)
	c.Assert(autoImported, Equals, true)
}

func (s *deviceMgrSuite) TestHandleAutoImportAssertionNoSeedCache(c *C) {
	a := devicestate.MockProcessAutoImportAssertion(func(*state.State, seed.Seed, asserts.RODatabase, func(batch *asserts.Batch) error) error {
		panic("trying to process auto-import-assertion without cached system seed")
	})
	defer a()

	s.mockSystemMode(c, "run")

	err := devicestate.EnsureAutoImportAssertions(s.mgr)
	c.Check(err, IsNil)

	// ensure state has not been changed
	s.state.Lock()
	defer s.state.Unlock()
	var autoImported bool
	err = s.state.Get("asserts-early-auto-imported", &autoImported)
	c.Assert(err, testutil.ErrorIs, state.ErrNoState)
	c.Assert(autoImported, Equals, false)
}

func (s *deviceMgrSuite) TestHandleAutoImportAssertionFailed(c *C) {
	a := devicestate.MockProcessAutoImportAssertion(func(*state.State, seed.Seed, asserts.RODatabase, func(batch *asserts.Batch) error) error {
		return fmt.Errorf("failed to add user from system user assertions")
	})
	defer a()

	s.mockSystemMode(c, "run")

	s.state.Lock()
	s.cacheDeviceCore20Seed(c)
	s.seeding()
	s.state.Unlock()

	logbuf, restore := logger.MockLogger()
	defer restore()

	err := s.mgr.Ensure()
	c.Check(err, IsNil)

	// ensure state has not been changed
	s.state.Lock()
	defer s.state.Unlock()
	var autoImported bool
	err = s.state.Get("asserts-early-auto-imported", &autoImported)
	c.Assert(err, IsNil)
	c.Assert(autoImported, Equals, true)
	c.Assert(logbuf.String(), testutil.Contains, `failed to add user from system user assertions`)
}

func (s *deviceMgrSuite) TestHandleAutoImportAssertionAlreadySeeded(c *C) {
	a := devicestate.MockProcessAutoImportAssertion(func(*state.State, seed.Seed, asserts.RODatabase, func(batch *asserts.Batch) error) error {
		return fmt.Errorf("failed to process auto import assertion")
	})
	defer a()

	s.mockSystemMode(c, "run")

	s.state.Lock()
	s.cacheDeviceCore20Seed(c)
	s.state.Set("seeded", true)
	s.state.Unlock()

	devicestate.SetEarlyBootLocaleConfigUpdatedRan(s.mgr, true)

	err := s.mgr.Ensure()
	c.Check(err, IsNil)

	// ensure state has not been changed
	s.state.Lock()
	defer s.state.Unlock()
	var autoImported bool
	err = s.state.Get("asserts-early-auto-imported", &autoImported)
	c.Assert(err, testutil.ErrorIs, state.ErrNoState)
	c.Assert(autoImported, Equals, false)
}

func (s *deviceMgrSuite) TestHandleAutoImportAssertionHappy(c *C) {

	s.mockSystemMode(c, "run")

	s.state.Lock()
	s.cacheDeviceCore20Seed(c)
	s.seeding()
	s.state.Unlock()

	err := s.mgr.Ensure()
	c.Check(err, IsNil)

	// check state is set as done
	s.state.Lock()
	defer s.state.Unlock()
	var autoImported bool
	err = s.state.Get("asserts-early-auto-imported", &autoImported)
	c.Assert(err, IsNil)
	c.Assert(autoImported, Equals, true)
}

func (s *deviceMgrSuite) TestDefaultRecoverySystem(c *C) {
	// no recovery system set
	s.state.Lock()
	s.state.Set("default-recovery-system", nil)
	s.state.Unlock()

	_, err := s.mgr.DefaultRecoverySystem()
	c.Assert(err, testutil.ErrorIs, state.ErrNoState)

	expectedSystem := devicestate.DefaultRecoverySystem{
		System:   "label",
		Model:    "model",
		BrandID:  "brand",
		Revision: 1,
	}

	// recovery system set
	s.state.Lock()
	s.state.Set("default-recovery-system", expectedSystem)
	s.state.Unlock()

	system, err := s.mgr.DefaultRecoverySystem()
	c.Assert(err, IsNil)
	c.Check(*system, Equals, expectedSystem)
}

func (s *deviceMgrSuite) TestSignConfdbControlNoSerial(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	_, err := s.mgr.SignConfdbControl([]any{}, 2)
	c.Assert(err, ErrorMatches, "cannot sign confdb-control without a serial")
}

func (s *deviceMgrSuite) TestSignConfdbControlNoKey(c *C) {
	s.setPCModelInState(c)
	s.state.Lock()
	defer s.state.Unlock()

	s.makeSerialAssertionInState(c, "canonical", "pc", "serialserialserial")

	_, err := s.mgr.SignConfdbControl([]any{}, 3)
	c.Assert(err, ErrorMatches, "cannot sign confdb-control without device key")
}

func (s *deviceMgrSuite) TestSignConfdbControlInvalid(c *C) {
	s.setPCModelInState(c)
	s.state.Lock()
	defer s.state.Unlock()

	s.makeSerialAssertionInState(c, "canonical", "pc", "serialserialserial")
	s.addKeyToManagerInState(c)

	groups := []any{map[string]any{"operators": []any{"jane"}}}
	_, err := s.mgr.SignConfdbControl(groups, 4)
	c.Assert(
		err,
		ErrorMatches,
		"cannot assemble assertion confdb-control: cannot parse group at position 1: \"authentications\" must be provided",
	)
}

func (s *deviceMgrSuite) TestSignConfdbControlOK(c *C) {
	s.setPCModelInState(c)
	s.state.Lock()
	defer s.state.Unlock()

	s.makeSerialAssertionInState(c, "canonical", "pc", "serialserialserial")
	s.addKeyToManagerInState(c)

	jane := map[string]any{
		"operators":       []any{"jane"},
		"authentications": []any{"operator-key"},
		"views": []any{
			"canonical/network/observe-interfaces",
			"canonical/network/control-interfaces",
		},
	}
	groups := []any{jane}

	cc, err := s.mgr.SignConfdbControl(groups, 5)
	c.Assert(err, IsNil)
	c.Assert(cc.Revision(), Equals, 5)

	// Confirm we can ack it
	assertstatetest.AddMany(s.state, cc)
}

func (s *deviceMgrSuite) TestConfdbControlNoSerial(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	_, err := s.mgr.ConfdbControl()
	c.Assert(err, ErrorMatches, "device has no identity yet")
}

func (s *deviceMgrSuite) TestConfdbControlNotFound(c *C) {
	s.setPCModelInState(c)
	s.state.Lock()
	defer s.state.Unlock()

	s.makeSerialAssertionInState(c, "canonical", "pc", "serialserialserial")
	s.addKeyToManagerInState(c)

	cc, err := s.mgr.ConfdbControl()
	c.Assert(cc, IsNil)
	c.Assert(err, ErrorMatches, "no state entry for key")
}

func (s *deviceMgrSuite) TestConfbControlUnknownSigningKey(c *C) {
	s.setPCModelInState(c)
	s.state.Lock()
	defer s.state.Unlock()

	s.makeSerialAssertionInState(c, "canonical", "pc", "serialserialserial")
	s.addKeyToManagerInState(c)

	cc, err := s.mgr.SignConfdbControl([]any{}, 10)
	c.Assert(err, IsNil)
	assertstatetest.AddMany(s.state, cc)

	// change the device key
	anotherKey, _ := assertstest.GenerateKey(testKeyLength)
	encDevKey, err := asserts.EncodePublicKey(anotherKey.PublicKey())
	c.Assert(err, IsNil)

	serial, err := s.brands.Signing("canonical").Sign(asserts.SerialType, map[string]any{
		"brand-id":            "canonical",
		"model":               "pc",
		"serial":              "serialserialserial",
		"device-key":          string(encDevKey),
		"device-key-sha3-384": anotherKey.PublicKey().ID(),
		"timestamp":           time.Now().Format(time.RFC3339),
		"revision":            "1",
	}, nil, "")
	c.Assert(err, IsNil)

	err = assertstate.Add(s.state, serial)
	c.Assert(err, IsNil)

	cc, err = s.mgr.ConfdbControl() // attempt to retrieve it
	c.Assert(cc, IsNil)
	c.Assert(err, ErrorMatches, "confdb-control's signing key doesn't match the device key")
}

func (s *deviceMgrSuite) TestConfdbControlFindExisting(c *C) {
	s.setPCModelInState(c)
	s.state.Lock()
	defer s.state.Unlock()

	s.makeSerialAssertionInState(c, "canonical", "pc", "serialserialserial")
	s.addKeyToManagerInState(c)

	// add assertion
	cc, err := s.mgr.SignConfdbControl([]any{}, 10)
	c.Assert(err, IsNil)
	assertstatetest.AddMany(s.state, cc)

	found, err := s.mgr.ConfdbControl()
	c.Assert(err, IsNil)
	c.Assert(found, DeepEquals, cc)
}

func (s *deviceMgrSuite) TestEnsureLoopLogging(c *C) {
	testutil.CheckEnsureLoopLogging("devicemgr.go", c, true)
}

func (s *deviceMgrSuite) TestSignResponseMessageOK(c *C) {
	s.setPCModelInState(c)
	s.state.Lock()
	defer s.state.Unlock()

	s.makeSerialAssertionInState(c, "canonical", "pc", "serialserialserial")
	s.addKeyToManagerInState(c)

	resAs, err := s.mgr.SignResponseMessage("landscape", "someId", asserts.MessageStatusSuccess, []byte("{}"))
	c.Assert(err, IsNil)
	c.Assert(resAs.AccountID(), Equals, "landscape")
	c.Assert(resAs.ID(), Equals, "someId")
	c.Assert(resAs.Status(), Equals, asserts.MessageStatusSuccess)
	c.Assert(resAs.Device().String(), Equals, "serialserialserial.pc.canonical")
}

func (s *deviceMgrSuite) TestSignResponseMessageNoSerial(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	_, err := s.mgr.SignResponseMessage("landscape", "someId", asserts.MessageStatusSuccess, []byte("{}"))
	c.Assert(err, ErrorMatches, "cannot sign response-message without a serial")
}

func (s *deviceMgrSuite) TestSignResponseMessageNoKey(c *C) {
	s.setPCModelInState(c)
	s.state.Lock()
	defer s.state.Unlock()

	s.makeSerialAssertionInState(c, "canonical", "pc", "serialserialserial")

	_, err := s.mgr.SignResponseMessage("landscape", "someId", asserts.MessageStatusSuccess, []byte("{}"))
	c.Assert(err, ErrorMatches, "cannot sign response-message without device key")
}

func (s *deviceMgrSuite) TestSignResponseMessageInvalid(c *C) {
	s.setPCModelInState(c)
	s.state.Lock()
	defer s.state.Unlock()

	s.makeSerialAssertionInState(c, "canonical", "pc", "serialserialserial")
	s.addKeyToManagerInState(c)

	_, err := s.mgr.SignResponseMessage("🤫", "someId", asserts.MessageStatusSuccess, []byte("{}"))
	c.Assert(
		err,
		ErrorMatches,
		"cannot assemble assertion response-message: invalid account id: 🤫",
	)
}

type myStateDeviceInitialized struct {
	called int
}

func (m *myStateDeviceInitialized) DeviceInitialized() {
	m.called++
}

func (s *deviceMgrSuite) TestDeviceManagerStartupCallbacks(c *C) {
	modeEnv := &boot.Modeenv{Mode: "run"}
	err := modeEnv.WriteTo("")
	c.Assert(err, IsNil)
	s.setUC20PCModelInState(c)

	mgr, err := devicestate.Manager(s.state, s.hookMgr, s.o.TaskRunner(), s.newStore)
	c.Assert(err, IsNil)

	callA := &myStateDeviceInitialized{called: 0}
	callB := &myStateDeviceInitialized{called: 0}
	mgr.AddOnInit(callA)
	mgr.AddOnInit(callB)

	sysctlCmd := testutil.MockCommand(c, "systemctl", "")
	defer sysctlCmd.Restore()

	mountCmd := testutil.MockCommand(c, "systemd-mount", "")
	defer mountCmd.Restore()

	err = mgr.StartUp()
	c.Assert(err, IsNil)
	c.Check(sysctlCmd.Calls(), HasLen, 0)
	c.Check(mountCmd.Calls(), HasLen, 0)

	c.Check(callA.called, Equals, 1)
	c.Check(callB.called, Equals, 1)
}

func (s *deviceMgrSuite) TestDeviceManagerEnsureFDE(c *C) {
	defer devicestate.MockSecbootMarkSuccessful(func() error {
		return fmt.Errorf("MarkSuccessful did not work")
	})()

	called := 0
	defer devicestate.MockFdestateAttemptAutoRepairIfNeeded(func(st *state.State, lockoutResetErr error) error {
		c.Check(lockoutResetErr, ErrorMatches, `MarkSuccessful did not work`)
		called++
		return nil
	})()

	devicestate.SetSystemMode(s.mgr, "run")
	err := devicestate.EnsureFDE(s.mgr)
	c.Assert(err, IsNil)
	c.Check(called, Equals, 1)
}

func (s *deviceMgrSuite) TestDeviceManagerEnsureFDEInstall(c *C) {
	defer devicestate.MockSecbootMarkSuccessful(func() error {
		c.Errorf("unexpected call")
		return fmt.Errorf("unexpected call")
	})()

	defer devicestate.MockFdestateAttemptAutoRepairIfNeeded(func(st *state.State, lockoutResetErr error) error {
		c.Errorf("unexpected call")
		return fmt.Errorf("unexpected call")
	})()

	devicestate.SetSystemMode(s.mgr, "install")
	err := devicestate.EnsureFDE(s.mgr)
	c.Assert(err, IsNil)
}
