// -*- 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 snapstate_test

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"sync"
	"time"

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

	"github.com/snapcore/snapd/advisor"
	"github.com/snapcore/snapd/asserts"
	"github.com/snapcore/snapd/asserts/assertstest"
	"github.com/snapcore/snapd/asserts/snapasserts"
	"github.com/snapcore/snapd/boot/boottest"
	"github.com/snapcore/snapd/bootloader"
	"github.com/snapcore/snapd/bootloader/bootloadertest"
	"github.com/snapcore/snapd/client"
	"github.com/snapcore/snapd/confdb"
	"github.com/snapcore/snapd/dirs"
	"github.com/snapcore/snapd/interfaces"
	"github.com/snapcore/snapd/interfaces/ifacetest"
	"github.com/snapcore/snapd/logger"
	"github.com/snapcore/snapd/osutil"
	"github.com/snapcore/snapd/overlord"
	"github.com/snapcore/snapd/overlord/assertstate"
	"github.com/snapcore/snapd/overlord/auth"
	"github.com/snapcore/snapd/overlord/hookstate"
	"github.com/snapcore/snapd/store/storetest"
	userclient "github.com/snapcore/snapd/usersession/client"

	// So it registers Configure.
	_ "github.com/snapcore/snapd/overlord/configstate"
	"github.com/snapcore/snapd/overlord/configstate/config"
	"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/backend"
	"github.com/snapcore/snapd/overlord/snapstate/sequence"
	"github.com/snapcore/snapd/overlord/snapstate/snapstatetest"
	"github.com/snapcore/snapd/overlord/state"
	"github.com/snapcore/snapd/release"
	"github.com/snapcore/snapd/snap"
	"github.com/snapcore/snapd/snap/naming"
	"github.com/snapcore/snapd/snap/snaptest"
	"github.com/snapcore/snapd/store"
	"github.com/snapcore/snapd/testutil"
)

func verifyUpdateTasks(c *C, typ snap.Type, opts, discards int, ts *state.TaskSet) {
	verifyUpdateTasksWithComponents(c, typ, opts, 0, discards, nil, ts)
}

func verifyUpdateTasksWithComponents(c *C, typ snap.Type, opts, compOpts, discards int, components []string, ts *state.TaskSet) {
	kinds := taskKinds(ts.Tasks())

	expected := expectedDoInstallTasks(typ, unlinkBefore|cleanupAfter|opts, compOpts, discards, nil, components, nil)
	if opts&doesReRefresh != 0 {
		if opts&mockDelayedEffects != 0 {
			expected = append(expected[:len(expected)-1], "check-rerefresh", expected[len(expected)-1])
		} else {
			expected = append(expected, "check-rerefresh")
		}
	}

	c.Assert(kinds, DeepEquals, expected)

	te := ts.MaybeEdge(snapstate.LastBeforeLocalModificationsEdge)
	c.Assert(te, NotNil)

	if len(components) == 0 {
		if opts&localSnap != 0 || opts&localRevision != 0 {
			c.Assert(te.Kind(), Equals, "prepare-snap")
		} else {
			c.Assert(te.Kind(), Equals, "validate-snap")
		}
	} else {
		if opts&compOptIsUnasserted == 0 {
			c.Assert(te.Kind(), Equals, "validate-component")
		} else {
			c.Assert(te.Kind(), Equals, "prepare-component")
		}
	}
}

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

func firstTaskAfterLocalModifications(c *C, ts *state.TaskSet) *state.Task {
	lastBeforeLocal, err := ts.Edge(snapstate.LastBeforeLocalModificationsEdge)
	c.Assert(err, IsNil)

	inSet := make(map[string]bool, len(ts.Tasks()))
	for _, t := range ts.Tasks() {
		inSet[t.ID()] = true
	}

	for _, ht := range lastBeforeLocal.HaltTasks() {
		if inSet[ht.ID()] {
			return ht
		}
	}

	c.Fatalf("cannot find first task after local modifications")
	return nil
}

func firstTaskAfterPrerequisites(c *C, ts *state.TaskSet) *state.Task {
	begin, err := ts.Edge(snapstate.BeginEdge)
	c.Assert(err, IsNil)

	inSet := make(map[string]bool, len(ts.Tasks()))
	for _, t := range ts.Tasks() {
		inSet[t.ID()] = true
	}

	for _, ht := range begin.HaltTasks() {
		if inSet[ht.ID()] {
			return ht
		}
	}

	c.Fatalf("cannot find first task after prerequisites")
	return nil
}

func firstTaskAfterMount(c *C, ts *state.TaskSet) *state.Task {
	mountTask := findKindInTaskSet(ts, "mount-snap")
	c.Assert(mountTask, NotNil)

	inSet := make(map[string]bool, len(ts.Tasks()))
	for _, t := range ts.Tasks() {
		inSet[t.ID()] = true
	}

	for _, ht := range mountTask.HaltTasks() {
		if inSet[ht.ID()] {
			return ht
		}
	}

	c.Fatalf("cannot find first task after mount-snap")
	return nil
}

func findMountSnap(c *C, ts *state.TaskSet) *state.Task {
	mountTask := findKindInTaskSet(ts, "mount-snap")
	c.Assert(mountTask, NotNil)
	return mountTask
}

func findSeedRefreshTaskSet(tss []*state.TaskSet) *state.TaskSet {
	for _, ts := range tss {
		for _, task := range ts.Tasks() {
			if task.Kind() == "create-recovery-system" {
				return ts
			}
		}
	}
	return nil
}

func hasDoRestartBoundary(task *state.Task) bool {
	var boundary restart.RestartBoundaryDirection
	if err := task.Get("restart-boundary", &boundary); err != nil {
		return false
	}
	return boundary == restart.RestartBoundaryDirectionDo
}

type seedRefreshSnap struct {
	name     string
	snapID   string
	snapType string
	base     string
	channel  string
	revision snap.Revision
}

func (s *snapmgrTestSuite) setupSeedRefreshUpdateTest(c *C, classic, enabled bool, model map[string]any, triggers []string) (observed *observedSeedRefreshCandidates, restore func()) {
	observed, restoreSeedRefreshHooks := mockSeedRefreshHooks(triggers)
	restores := []func(){
		release.MockOnClassic(classic),
		snapstatetest.MockDeviceModel(MakeModel(model)),
		restoreSeedRefreshHooks,
	}

	if enabled {
		s.state.Lock()
		tr := config.NewTransaction(s.state)
		c.Assert(tr.Set("core", "experimental.seed-refresh", true), IsNil)
		tr.Commit()
		s.state.Unlock()
	}

	return observed, func() {
		for i := len(restores) - 1; i >= 0; i-- {
			restores[i]()
		}
	}
}

func (s *snapmgrTestSuite) installSeedRefreshSnaps(c *C, specs ...seedRefreshSnap) {
	for _, spec := range specs {
		rev := spec.revision
		if rev == snap.R(0) {
			rev = snap.R(7)
		}

		si := snap.SideInfo{
			RealName: spec.name,
			Revision: rev,
			SnapID:   spec.snapID,
		}

		var snapYaml strings.Builder
		fmt.Fprintf(&snapYaml, "name: %s\n", spec.name)
		if spec.snapType != "" {
			fmt.Fprintf(&snapYaml, "type: %s\n", spec.snapType)
		}
		if spec.base != "" {
			fmt.Fprintf(&snapYaml, "base: %s\n", spec.base)
		}

		snaptest.MockSnap(c, snapYaml.String(), &si)

		trackingChannel := spec.channel
		if trackingChannel == "" {
			trackingChannel = "latest/stable"
		}

		s.fakeStore.registerID(si.RealName, si.SnapID)
		snapstate.Set(s.state, si.RealName, &snapstate.SnapState{
			Active:          true,
			Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si}),
			Current:         si.Revision,
			TrackingChannel: trackingChannel,
			SnapType:        spec.snapType,
		})
	}
}

func runSeedRefreshUpdate(c *C, st *state.State, userID int, updates []snapstate.StoreUpdate) (*snapstate.UpdateTaskSets, *state.Change) {
	goal := snapstate.StoreUpdateGoal(updates...)

	_, uts, err := snapstate.UpdateWithGoal(context.Background(), st, goal, nil, snapstate.Options{
		UserID: userID,
		Flags: snapstate.Flags{
			Transaction: client.TransactionPerSnap,
		},
	})
	c.Assert(err, IsNil)

	chg := st.NewChange("refresh", "refresh")
	for _, ts := range uts.Refresh {
		chg.AddAll(ts)
	}

	c.Assert(chg.CheckTaskDependencies(), IsNil)

	return uts, chg
}

func parseSeedRefreshTaskSets(uts *snapstate.UpdateTaskSets) (map[string]*state.TaskSet, *state.TaskSet) {
	taskSetsBySnap := make(map[string]*state.TaskSet)
	for _, ts := range uts.Refresh {
		for _, t := range ts.Tasks() {
			snapsup, err := snapstate.TaskSnapSetup(t)
			if err != nil {
				continue
			}

			taskSetsBySnap[snapsup.InstanceName()] = ts
			break
		}
	}

	return taskSetsBySnap, findSeedRefreshTaskSet(uts.Refresh)
}

func seedRefreshCandidateFromTaskSet(c *C, ts *state.TaskSet) snapstate.SeedRefreshCandidate {
	t, err := ts.Edge(snapstate.SnapSetupEdge)
	c.Assert(err, IsNil)

	snapsup, err := snapstate.TaskSnapSetup(t)
	c.Assert(err, IsNil)

	candidate := snapstate.SeedRefreshCandidate{
		InstanceName: snapsup.InstanceName(),
	}
	if !snapsup.ComponentExclusiveOperation {
		candidate.SnapSetupTaskIDs = append(candidate.SnapSetupTaskIDs, t.ID())
	}

	if err := t.Get("component-setup-tasks", &candidate.ComponentSetupTaskIDs); err != nil && !errors.Is(err, state.ErrNoState) {
		c.Assert(err, IsNil)
	}

	return candidate
}

func mustTaskSetForSnap(c *C, taskSetsBySnap map[string]*state.TaskSet, snapName string) *state.TaskSet {
	ts := taskSetsBySnap[snapName]
	c.Assert(ts, NotNil, Commentf("missing task set for %q", snapName))
	return ts
}

func splitSeedRefreshTasks(c *C, seedTS *state.TaskSet) (create, finalize *state.Task, removals []*state.Task) {
	for _, task := range seedTS.Tasks() {
		switch task.Kind() {
		case "create-recovery-system":
			create = task
		case "finalize-recovery-system":
			finalize = task
		case "remove-recovery-system":
			removals = append(removals, task)
		}
	}
	c.Assert(create, NotNil)
	c.Assert(finalize, NotNil)
	return create, finalize, removals
}

func waitTasksContainKindForSnap(c *C, waiter *state.Task, instanceName, kind string) bool {
	for _, task := range waiter.WaitTasks() {
		if task.Kind() != kind {
			continue
		}

		snapsup, err := snapstate.TaskSnapSetup(task)
		c.Assert(err, IsNil)
		if snapsup.InstanceName() == instanceName {
			return true
		}
	}
	return false
}

func countTasksOfKind(tasks []*state.Task, kind string) int {
	var count int
	for _, task := range tasks {
		if task.Kind() == kind {
			count++
		}
	}
	return count
}

func assertTaskSetStatus(c *C, ts *state.TaskSet, expected state.Status) {
	for _, t := range ts.Tasks() {
		c.Check(t.Status(), Equals, expected)
	}
}

func mockSeedRefreshRebootHandlers(s *snapmgrTestSuite, c *C, finalizeErr error) *[]restart.RestartType {
	s.o.TaskRunner().AddHandler("create-recovery-system", func(task *state.Task, _ *tomb.Tomb) error {
		task.State().Lock()
		defer task.State().Unlock()
		return restart.FinishTaskWithRestart(task, state.DoneStatus, restart.RestartSystem, "", nil)
	}, func(task *state.Task, tomb *tomb.Tomb) error { return nil })

	s.o.TaskRunner().AddHandler("finalize-recovery-system", func(*state.Task, *tomb.Tomb) error {
		return finalizeErr
	}, func(task *state.Task, tomb *tomb.Tomb) error { return nil })

	var restartRequested []restart.RestartType
	_, err := restart.Manager(s.state, "boot-id-0", snapstatetest.MockRestartHandler(func(t restart.RestartType) {
		restartRequested = append(restartRequested, t)
	}))
	c.Assert(err, IsNil)

	return &restartRequested
}

func assertTaskKindStatus(c *C, ts *state.TaskSet, kind string, expected state.Status) {
	var task *state.Task
	for _, t := range ts.Tasks() {
		if t.Kind() == kind {
			task = t
			break
		}
	}
	c.Assert(task, NotNil)
	c.Check(task.Status(), Equals, expected)
}

func assertLinkTaskStatus(c *C, ts *state.TaskSet, expected state.Status) {
	assertTaskKindStatus(c, ts, "link-snap", expected)
}

func (s *snapmgrTestSuite) TestUpdateDoesGC(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	restore := release.MockOnClassic(false)
	defer restore()

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)},
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(2)},
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(3)},
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(4)},
		}),
		Current:  snap.R(4),
		SnapType: "app",
	})

	chg := s.state.NewChange("update", "update a snap")
	ts, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "some-channel"}, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)
	chg.AddAll(ts)

	s.settle(c)

	// ensure garbage collection runs as the last tasks
	expectedTail := fakeOps{
		{
			op:   "link-snap",
			path: filepath.Join(dirs.SnapMountDir, "some-snap/11"),
		},
		{
			op: "maybe-set-next-boot",
		},
		{
			op:    "auto-connect:Doing",
			name:  "some-snap",
			revno: snap.R(11),
		},
		{
			op: "update-aliases",
		},
		{
			op:    "list-non-snapctl-mounts-rev",
			name:  "some-snap",
			revno: snap.R(1),
		},
		{
			op:     "remove-snap-mount-units",
			name:   "some-snap",
			origin: "mount-control",
			dirs:   []string{snap.DataDir("some-snap", snap.R(1))},
		},
		{
			op:   "remove-snap-data",
			path: filepath.Join(dirs.SnapMountDir, "some-snap/1"),
		},
		{
			op:    "remove-snap-files",
			path:  filepath.Join(dirs.SnapMountDir, "some-snap/1"),
			stype: "app",
		},
		{
			op:    "list-non-snapctl-mounts-rev",
			name:  "some-snap",
			revno: snap.R(2),
		},
		{
			op:     "remove-snap-mount-units",
			name:   "some-snap",
			origin: "mount-control",
			dirs:   []string{snap.DataDir("some-snap", snap.R(2))},
		},
		{
			op:   "remove-snap-data",
			path: filepath.Join(dirs.SnapMountDir, "some-snap/2"),
		},
		{
			op:    "remove-snap-files",
			path:  filepath.Join(dirs.SnapMountDir, "some-snap/2"),
			stype: "app",
		},
		{
			op:    "cleanup-trash",
			name:  "some-snap",
			revno: snap.R(11),
		},
	}

	opsTail := s.fakeBackend.ops[len(s.fakeBackend.ops)-len(expectedTail):]
	c.Assert(opsTail.Ops(), DeepEquals, expectedTail.Ops())
	c.Check(opsTail, DeepEquals, expectedTail)
}

func (s *snapmgrTestSuite) TestRepeatedUpdatesDoGC(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	restore := release.MockOnClassic(false)
	defer restore()

	// start with a single revision
	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-snap", SnapID: "some-snap-id", Channel: "some-channel", Revision: snap.R(1)},
		}),
		Current:  snap.R(1),
		SnapType: "app",
	})

	tr := config.NewTransaction(s.state)
	// allow 2 revisions
	c.Assert(tr.Set("core", "refresh.retain", 2), IsNil)
	tr.Commit()

	s.fakeStore.refreshRevnos = make(map[string]snap.Revision)

	for refreshRev := 2; refreshRev < 10; refreshRev++ {
		s.fakeStore.refreshRevnos["some-snap-id"] = snap.R(refreshRev)

		chg := s.state.NewChange("update", "update a snap")
		ts, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "some-channel"}, s.user.ID, snapstate.Flags{})
		c.Assert(err, IsNil)
		chg.AddAll(ts)

		s.settle(c)

		var snapst snapstate.SnapState
		c.Assert(snapstate.Get(s.state, "some-snap", &snapst), IsNil)
		// and we expect 2 revisions at all times
		c.Check(snapst.Sequence.Revisions, HasLen, 2)
		c.Check(snapst.Sequence, DeepEquals, snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-snap", SnapID: "some-snap-id", Channel: "some-channel", Revision: snap.R(refreshRev - 1)},
			{RealName: "some-snap", SnapID: "some-snap-id", Channel: "some-channel", Revision: snap.R(refreshRev)},
		}))
	}
}

func (s *snapmgrTestSuite) TestUpdateScenarios(c *C) {
	// TODO: also use channel-for-7 or equiv to check updates that are switches
	for k, t := range switchScenarios {
		s.testUpdateScenario(c, k, t)
	}
}

func (s *snapmgrTestSuite) testUpdateScenario(c *C, desc string, t switchScenario) {
	// reset
	s.fakeBackend.ops = nil

	comment := Commentf("%q (%+v)", desc, t)
	si := snap.SideInfo{
		RealName: "some-snap",
		Revision: snap.R(7),
		Channel:  t.chanFrom,
		SnapID:   "some-snap-id",
	}

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

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si}),
		Active:          true,
		Current:         si.Revision,
		TrackingChannel: t.chanFrom,
		CohortKey:       t.cohFrom,
	})

	chg := s.state.NewChange("update-snap", t.summary)
	ts, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{
		Channel:     t.chanTo,
		CohortKey:   t.cohTo,
		LeaveCohort: t.cohFrom != "" && t.cohTo == "",
	}, 0, snapstate.Flags{})
	c.Assert(err, IsNil, comment)
	chg.AddAll(ts)

	s.settle(c)

	// switch is not really really doing anything backend related
	c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, []string{
		"storesvc-snap-action",
		"storesvc-snap-action:action",
		"storesvc-download",
		"validate-snap:Doing",
		"current",
		"open-snap-file",
		"setup-snap",
		"remove-snap-aliases",
		"run-inhibit-snap-for-unlink",
		"discard-namespace-locked",
		"unlink-snap",
		"copy-data",
		"setup-snap-save-data",
		"setup-profiles:Doing",
		"candidate",
		"link-snap",
		"maybe-set-next-boot",
		"auto-connect:Doing",
		"update-aliases",
		"cleanup-trash",
	}, comment)

	expectedChanTo := t.chanTo
	if t.chanTo == "" {
		expectedChanTo = t.chanFrom
	}
	expectedCohTo := t.cohTo

	// ensure the desired channel/cohort has changed
	var snapst snapstate.SnapState
	err = snapstate.Get(s.state, "some-snap", &snapst)
	c.Assert(err, IsNil, comment)
	c.Assert(snapst.TrackingChannel, Equals, expectedChanTo, comment)
	c.Assert(snapst.CohortKey, Equals, expectedCohTo, comment)

	// ensure the current info *has* changed
	info, err := snapst.CurrentInfo()
	c.Assert(err, IsNil, comment)
	c.Assert(info.Channel, Equals, expectedChanTo, comment)
}

func (s *snapmgrTestSuite) TestUpdateTasksWithOldCurrent(c *C) {
	s.state.Lock()
	defer s.state.Unlock()
	restore := release.MockOnClassic(false)
	defer restore()

	si1 := &snap.SideInfo{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)}
	si2 := &snap.SideInfo{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(2)}
	si3 := &snap.SideInfo{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(3)}
	si4 := &snap.SideInfo{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(4)}
	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:          true,
		TrackingChannel: "latest/edge",
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si1, si2, si3, si4}),
		Current:         snap.R(2),
		SnapType:        "app",
	})

	// run the update
	ts, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "some-channel"}, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)

	verifyUpdateTasks(c, snap.TypeApp, doesReRefresh|mockDelayedEffects, 2, ts)

	// and ensure that it will remove the revisions after "current"
	// (si3, si4)
	var snapsup snapstate.SnapSetup
	tasks := ts.Tasks()

	// TODO use a helper to find proper tasks
	i := len(tasks) - 9
	c.Check(tasks[i].Kind(), Equals, "clear-snap")
	err = tasks[i].Get("snap-setup", &snapsup)
	c.Assert(err, IsNil)
	c.Check(snapsup.Revision(), Equals, si3.Revision)

	i = len(tasks) - 7
	c.Check(tasks[i].Kind(), Equals, "clear-snap")
	err = tasks[i].Get("snap-setup", &snapsup)
	c.Assert(err, IsNil)
	c.Check(snapsup.Revision(), Equals, si4.Revision)
}

func (s *snapmgrTestSuite) enableRefreshAppAwarenessUX() {
	s.state.Lock()
	tr := config.NewTransaction(s.state)
	tr.Set("core", "experimental.refresh-app-awareness-ux", true)
	tr.Commit()
	s.state.Unlock()
}

func (s *snapmgrTestSuite) testUpdateCanDoBackwards(c *C, refreshAppAwarenessUX bool) {
	si7 := snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Revision: snap.R(7),
	}
	si11 := snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Revision: snap.R(11),
	}

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

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si7, &si11}),
		Current:  si11.Revision,
		SnapType: "app",
	})

	chg := s.state.NewChange("refresh", "refresh a snap")
	ts, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Revision: snap.R(7)}, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)
	chg.AddAll(ts)

	s.settle(c)

	expected := fakeOps{
		{
			op:   "remove-snap-aliases",
			name: "some-snap",
		},
		{
			op:          "run-inhibit-snap-for-unlink",
			name:        "some-snap",
			inhibitHint: "refresh",
		},
		{
			op:   "discard-namespace-locked",
			name: "some-snap",
		},
		{
			op:                 "unlink-snap",
			path:               filepath.Join(dirs.SnapMountDir, "some-snap/11"),
			unlinkSkipBinaries: refreshAppAwarenessUX,
			inhibitHint:        "refresh",
		},
		{
			op:   "copy-data",
			path: filepath.Join(dirs.SnapMountDir, "some-snap/7"),
			old:  filepath.Join(dirs.SnapMountDir, "some-snap/11"),
		},
		{
			op:   "setup-snap-save-data",
			path: filepath.Join(dirs.SnapDataSaveDir, "some-snap"),
		},
		{
			op:    "setup-profiles:Doing",
			name:  "some-snap",
			revno: snap.R(7),
		},
		{
			op: "candidate",
			sinfo: snap.SideInfo{
				RealName: "some-snap",
				SnapID:   "some-snap-id",
				Channel:  "",
				Revision: snap.R(7),
			},
		},
		{
			op:   "link-snap",
			path: filepath.Join(dirs.SnapMountDir, "some-snap/7"),
		},
		{
			op: "maybe-set-next-boot",
		},
		{
			op:    "auto-connect:Doing",
			name:  "some-snap",
			revno: snap.R(7),
		},
		{
			op: "update-aliases",
		},
		{
			op:    "cleanup-trash",
			name:  "some-snap",
			revno: snap.R(7),
		},
	}
	// aliases removal is skipped when refresh-app-awareness-ux is enabled
	if refreshAppAwarenessUX {
		// remove "remove-snap-aliases" operation
		expected = expected[1:]
	}
	// start with an easier-to-read error if this fails:
	c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
	c.Assert(s.fakeBackend.ops, DeepEquals, expected)
}

func (s *snapmgrTestSuite) TestUpdateCanDoBackwards(c *C) {
	s.testUpdateCanDoBackwards(c, false)
}

func (s *snapmgrTestSuite) TestUpdateCanDoBackwardsSkipBinaries(c *C) {
	s.enableRefreshAppAwarenessUX()
	s.testUpdateCanDoBackwards(c, true)
}

func revs(seq []*sequence.RevisionSideState) []int {
	revs := make([]int, len(seq))
	for i, si := range seq {
		revs[i] = si.Snap.Revision.N
	}

	return revs
}

type opSeqOpts struct {
	revert  bool
	fail    bool
	before  []int
	current int
	via     int
	after   []int
}

// build a SnapState with a revision sequence given by `before` and a
// current revision of `current`. Then refresh --revision via. Then
// check the revision sequence is as in `after`.
func (s *snapmgrTestSuite) testOpSequence(c *C, opts *opSeqOpts) (*snapstate.SnapState, *state.TaskSet) {
	s.state.Lock()
	defer s.state.Unlock()

	seq := make([]*snap.SideInfo, len(opts.before))
	for i, n := range opts.before {
		seq[i] = &snap.SideInfo{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(n)}
	}

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:          true,
		TrackingChannel: "latest/edge",
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos(seq),
		Current:         snap.R(opts.current),
		SnapType:        "app",
	})

	var chg *state.Change
	var ts *state.TaskSet
	var err error
	if opts.revert {
		chg = s.state.NewChange("revert", "revert a snap")
		ts, err = snapstate.RevertToRevision(s.state, "some-snap", snap.R(opts.via), snapstate.Flags{}, "")
	} else {
		chg = s.state.NewChange("refresh", "refresh a snap")
		ts, err = snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Revision: snap.R(opts.via)}, s.user.ID, snapstate.Flags{})
	}
	c.Assert(err, IsNil)
	if opts.fail {
		tasks := ts.Tasks()
		var last *state.Task
		// don't make a task wait on rerefresh, that's bad
		for i := len(tasks) - 1; i > 0; i-- {
			last = tasks[i]
			if kind := last.Kind(); kind != "check-rerefresh" && kind != "mock-process-delayed-security-backend-effects" {
				break
			}
		}
		terr := s.state.NewTask("error-trigger", "provoking total undo")
		terr.WaitFor(last)
		if len(last.Lanes()) > 0 {
			lanes := last.Lanes()
			// validity
			c.Assert(lanes, HasLen, 1)
			terr.JoinLane(lanes[0])
		}
		chg.AddTask(terr)
	}
	chg.AddAll(ts)

	s.settle(c)

	var snapst snapstate.SnapState
	err = snapstate.Get(s.state, "some-snap", &snapst)
	c.Assert(err, IsNil)
	c.Check(revs(snapst.Sequence.Revisions), DeepEquals, opts.after)

	return &snapst, ts
}

func (s *snapmgrTestSuite) testUpdateSequence(c *C, opts *opSeqOpts) *state.TaskSet {
	restore := release.MockOnClassic(false)
	defer restore()

	opts.revert = false
	snapst, ts := s.testOpSequence(c, opts)
	// update always ends with current==seq[-1]==via:
	c.Check(snapst.Current.N, Equals, opts.after[len(opts.after)-1])
	c.Check(snapst.Current.N, Equals, opts.via)

	c.Check(s.fakeBackend.ops.Count("copy-data"), Equals, 1)
	c.Check(s.fakeBackend.ops.First("copy-data"), DeepEquals, &fakeOp{
		op:   "copy-data",
		path: fmt.Sprintf(filepath.Join(dirs.SnapMountDir, "some-snap/%d"), opts.via),
		old:  fmt.Sprintf(filepath.Join(dirs.SnapMountDir, "some-snap/%d"), opts.current),
	})

	return ts
}

func (s *snapmgrTestSuite) testUpdateFailureSequence(c *C, opts *opSeqOpts) *state.TaskSet {
	restore := release.MockOnClassic(false)
	defer restore()

	opts.revert = false
	opts.after = opts.before
	s.fakeBackend.linkSnapFailTrigger = fmt.Sprintf(filepath.Join(dirs.SnapMountDir, "some-snap/%d"), opts.via)
	snapst, ts := s.testOpSequence(c, opts)
	// a failed update will always end with current unchanged
	c.Check(snapst.Current.N, Equals, opts.current)

	ops := s.fakeBackend.ops
	c.Check(ops.Count("copy-data"), Equals, 1)
	do := ops.First("copy-data")

	c.Check(ops.Count("undo-copy-snap-data"), Equals, 1)
	undo := ops.First("undo-copy-snap-data")

	do.op = undo.op
	c.Check(do, DeepEquals, undo) // i.e. they only differed in the op

	return ts
}

// testTotal*Failure fails *after* link-snap
func (s *snapmgrTestSuite) testTotalUpdateFailureSequence(c *C, opts *opSeqOpts) *state.TaskSet {
	restore := release.MockOnClassic(false)
	defer restore()

	opts.revert = false
	opts.fail = true
	snapst, ts := s.testOpSequence(c, opts)
	// a failed update will always end with current unchanged
	c.Check(snapst.Current.N, Equals, opts.current)

	ops := s.fakeBackend.ops
	c.Check(ops.Count("copy-data"), Equals, 1)
	do := ops.First("copy-data")

	c.Check(ops.Count("undo-copy-snap-data"), Equals, 1)
	undo := ops.First("undo-copy-snap-data")

	do.op = undo.op
	c.Check(do, DeepEquals, undo) // i.e. they only differed in the op

	return ts
}

func (s *snapmgrTestSuite) TestUpdateFailsEarlyOnEpochMismatch(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	snapstate.Set(s.state, "some-epoch-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-epoch-snap", SnapID: "some-epoch-snap-id", Revision: snap.R(1)},
		}),
		Current:  snap.R(1),
		SnapType: "app",
	})

	_, err := snapstate.Update(s.state, "some-epoch-snap", nil, 0, snapstate.Flags{})
	c.Assert(err, ErrorMatches, `cannot refresh "some-epoch-snap" to new revision 11 with epoch 42, because it can't read the current epoch of 13`)
}

func (s *snapmgrTestSuite) TestUpdateTasksPropagatesErrors(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:          true,
		TrackingChannel: "latest/edge",
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{RealName: "some-snap", SnapID: "fakestore-please-error-on-refresh", Revision: snap.R(7)}}),
		Current:         snap.R(7),
	})

	_, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "some-channel"}, s.user.ID, snapstate.Flags{})
	c.Assert(err, ErrorMatches, `failing as requested`)
}

func (s *snapmgrTestSuite) TestUpdateTasks(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:          true,
		TrackingChannel: "latest/edge",
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(7)}}),
		Current:         snap.R(7),
		SnapType:        "app",
	})

	validateCalled := false
	happyValidateRefreshes := func(st *state.State, refreshes []*snap.Info, ignoreValidation map[string]bool, userID int, deviceCtx snapstate.DeviceContext) ([]*snap.Info, error) {
		validateCalled = true
		return refreshes, nil
	}
	// hook it up
	snapstate.ValidateRefreshes = happyValidateRefreshes

	ts, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "some-channel"}, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)
	verifyUpdateTasks(c, snap.TypeApp, doesReRefresh|mockDelayedEffects, 0, ts)
	c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks()))

	c.Check(validateCalled, Equals, true)

	var snapsup snapstate.SnapSetup
	err = ts.Tasks()[0].Get("snap-setup", &snapsup)
	c.Assert(err, IsNil)

	c.Check(snapsup.Channel, Equals, "some-channel")
}

func (s *snapmgrTestSuite) TestUpdatePrerequisitesSyncTask(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:          true,
		TrackingChannel: "latest/edge",
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(7)}}),
		Current:         snap.R(7),
		SnapType:        "app",
	})

	ts, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "some-channel"}, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)

	chg := s.state.NewChange("refresh-snap", "test: update snap")
	chg.AddAll(ts)

	earlyPrereq, syncPrereq := findPrereqTasksForSnap(c, chg, "some-snap")

	c.Check(earlyPrereq.Has("snap-setup"), Equals, true)
	c.Check(earlyPrereq.Has("snap-setup-task"), Equals, false)

	c.Check(syncPrereq.Has("snap-setup"), Equals, false)
	c.Check(syncPrereq.Has("snap-setup-task"), Equals, true)
	c.Check(firstTaskAfterLocalModifications(c, ts), Equals, syncPrereq)
}

func (s *snapmgrTestSuite) TestUpdateAmendRunThrough(c *C) {
	const tryMode = false
	s.testUpdateAmendRunThrough(c, tryMode, nil)
}

func (s *snapmgrTestSuite) TestUpdateAmendRunThroughTryMode(c *C) {
	const tryMode = true
	s.testUpdateAmendRunThrough(c, tryMode, nil)
}

func (s *snapmgrTestSuite) TestUpdateAmendWithComponentsRunThroughTryMode(c *C) {
	const tryMode = true
	s.testUpdateAmendRunThrough(c, tryMode, []string{"standard-component", "kernel-modules-component"})
}

func (s *snapmgrTestSuite) TestUpdateAmendWithComponentsRunThrough(c *C) {
	const tryMode = false
	s.testUpdateAmendRunThrough(c, tryMode, []string{"standard-component", "kernel-modules-component"})
}

func (s *snapmgrTestSuite) testUpdateAmendRunThrough(c *C, tryMode bool, components []string) {
	si := snap.SideInfo{
		RealName: "some-kernel",
		Revision: snap.R(-42),
	}
	snaptest.MockSnap(c, `name: some-kernel`, &si)

	r := snapstatetest.MockDeviceModel(MakeModel20("pc", map[string]any{"base": "core24"}))
	defer r()

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

	sort.Strings(components)

	originalCompRevisions := make(map[string]snap.Revision)
	for i, compName := range components {
		originalCompRevisions[compName] = snap.R(-(i + 1))
	}

	updatedCompRevisions := make(map[string]snap.Revision)
	for _, compName := range components {
		updatedCompRevisions[compName] = snap.R(-originalCompRevisions[compName].N)
	}

	s.fakeStore.snapResourcesFn = func(info *snap.Info) []store.SnapResourceResult {
		var results []store.SnapResourceResult
		for _, compName := range components {
			results = append(results, store.SnapResourceResult{
				DownloadInfo: snap.DownloadInfo{
					DownloadURL: "http://example.com/" + compName,
				},
				Name:      compName,
				Revision:  updatedCompRevisions[compName].N,
				Type:      fmt.Sprintf("component/%s", componentNameToType(c, compName)),
				Version:   "1.0",
				CreatedAt: "2024-01-01T00:00:00Z",
			})
		}
		return results
	}

	currentSeq := snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si})

	currentResources := make(map[string]snap.Revision, len(components))
	for _, comp := range components {
		err := currentSeq.AddComponentForRevision(si.Revision, &sequence.ComponentState{
			SideInfo: &snap.ComponentSideInfo{
				Component: naming.NewComponentRef(si.RealName, comp),
				Revision:  originalCompRevisions[comp],
			},
			CompType: componentNameToType(c, comp),
		})
		c.Assert(err, IsNil)

		if currentResources == nil {
			currentResources = make(map[string]snap.Revision, len(components))
		}
		currentResources[comp] = originalCompRevisions[comp]
	}

	currentComponentStates := currentSeq.Revisions[0].Components
	currentKmodComps := make([]*snap.ComponentSideInfo, 0, len(currentComponentStates))
	for _, cs := range currentComponentStates {
		if cs.CompType == snap.KernelModulesComponent {
			currentKmodComps = append(currentKmodComps, cs.SideInfo)
		}
	}

	var expectedComponentStates []*sequence.ComponentState
	for _, comp := range components {
		expectedComponentStates = append(expectedComponentStates, &sequence.ComponentState{
			SideInfo: &snap.ComponentSideInfo{
				Component: naming.NewComponentRef(si.RealName, comp),
				Revision:  updatedCompRevisions[comp],
			},
			CompType: componentNameToType(c, comp),
		})
	}

	newKmodComps := make([]*snap.ComponentSideInfo, 0, len(expectedComponentStates))
	for _, cs := range expectedComponentStates {
		if cs.CompType == snap.KernelModulesComponent {
			newKmodComps = append(newKmodComps, cs.SideInfo)
		}
	}

	s.AddCleanup(snapstate.MockReadComponentInfo(func(
		compMntDir string, info *snap.Info, csi *snap.ComponentSideInfo,
	) (*snap.ComponentInfo, error) {
		return &snap.ComponentInfo{
			Component:         csi.Component,
			Type:              componentNameToType(c, csi.Component.ComponentName),
			CompVersion:       "1.0",
			ComponentSideInfo: *csi,
		}, nil
	}))

	snapstate.Set(s.state, "some-kernel", &snapstate.SnapState{
		Active:          true,
		Sequence:        currentSeq,
		Current:         si.Revision,
		SnapType:        "app",
		TrackingChannel: "channel-for-components",
		Flags:           snapstate.Flags{TryMode: tryMode},
	})

	chg := s.state.NewChange("refresh", "refresh a snap")
	ts, err := snapstate.Update(s.state, "some-kernel", &snapstate.RevisionOptions{
		Channel: "channel-for-components",
	}, s.user.ID, snapstate.Flags{Amend: true})
	c.Assert(err, IsNil)
	chg.AddAll(ts)

	s.settle(c)

	downloads := []fakeDownload{{
		macaroon: s.user.StoreMacaroon,
		name:     "some-kernel",
		target:   filepath.Join(dirs.SnapBlobDir, "some-kernel_11.snap"),
	}}

	for _, compName := range components {
		downloads = append(downloads, fakeDownload{
			macaroon: s.user.StoreMacaroon,
			name:     fmt.Sprintf("%s+%s", si.RealName, compName),
			target:   filepath.Join(dirs.SnapBlobDir, fmt.Sprintf("%s+%s_%d.comp", si.RealName, compName, updatedCompRevisions[compName].N)),
		})
	}

	c.Check(s.fakeStore.downloads, DeepEquals, downloads)

	c.Check(s.fakeStore.seenPrivacyKeys["privacy-key"], Equals, true, Commentf("salts seen: %v", s.fakeStore.seenPrivacyKeys))
	ops := []string{
		"storesvc-snap-action",
		"storesvc-snap-action:action",
		"storesvc-download",
		"validate-snap:Doing",
	}
	for range components {
		ops = append(ops, []string{
			"storesvc-download",
			"validate-component:Doing",
		}...)
	}
	ops = append(ops, []string{
		"current",
		"open-snap-file",
		"setup-snap",
	}...)
	for range components {
		ops = append(ops, "setup-component")
	}
	ops = append(ops, []string{
		"remove-snap-aliases",
		"run-inhibit-snap-for-unlink",
		"discard-namespace-locked",
		"unlink-snap",
		"prepare-kernel-snap",
		"update-gadget-assets:Doing",
		"copy-data",
		"setup-snap-save-data",
		"setup-profiles:Doing",
		"candidate",
		"link-snap",
	}...)
	if len(currentKmodComps) == 0 {
		ops = append(ops, "maybe-set-next-boot")
	}
	for range components {
		ops = append(ops, "link-component")
	}
	ops = append(ops, []string{
		"auto-connect:Doing",
		"update-aliases",
	}...)
	if len(currentKmodComps) > 0 {
		ops = append(ops, "prepare-kernel-modules-components", "maybe-set-next-boot")
	}
	ops = append(ops, []string{
		"remove-kernel-snap-setup",
		"cleanup-trash",
	}...)

	c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, ops)

	// just check the interesting op
	c.Check(s.fakeBackend.ops[1], DeepEquals, fakeOp{
		op: "storesvc-snap-action:action",
		action: store.SnapAction{
			Action:       "install", // we asked for an Update, but an amend is actually an Install
			InstanceName: "some-kernel",
			Channel:      "channel-for-components",
			Epoch:        snap.E("1*"), // in amend, epoch in the action is not nil!
		},
		revno:  snap.R(11),
		userID: 1,
	})

	task := ts.Tasks()[1]
	// verify snapSetup info
	var snapsup snapstate.SnapSetup
	err = task.Get("snap-setup", &snapsup)
	c.Assert(err, IsNil)
	c.Assert(snapsup, DeepEquals, snapstate.SnapSetup{
		Channel: "channel-for-components",
		UserID:  s.user.ID,

		DownloadInfo: &snap.DownloadInfo{
			DownloadURL: "https://some-server.com/some/path.snap",
			Size:        5,
		},
		SideInfo:  snapsup.SideInfo,
		Type:      snap.TypeKernel,
		Version:   "some-kernelVer",
		PlugsOnly: true,
		Flags: snapstate.Flags{
			Amend:       true,
			Transaction: client.TransactionPerSnap,
		},
		PreUpdateKernelModuleComponents: currentKmodComps,
	})
	c.Assert(snapsup.SideInfo, DeepEquals, &snap.SideInfo{
		RealName: "some-kernel",
		Revision: snap.R(11),
		Channel:  "channel-for-components",
		SnapID:   "some-kernel-id",
	})

	// verify services stop reason
	verifyStopReason(c, ts, "refresh")

	// check post-refresh hook
	var postRefreshHookTask *state.Task
	for _, t := range ts.Tasks() {
		if t.Kind() != "run-hook" {
			continue
		}
		if strings.Contains(t.Summary(), `Run post-refresh hook of "some-kernel" snap if present`) {
			postRefreshHookTask = t
			break
		}
	}
	c.Assert(postRefreshHookTask, NotNil, Commentf(printTasks(ts.Tasks())))

	// verify snaps in the system state
	var snapst snapstate.SnapState
	err = snapstate.Get(s.state, "some-kernel", &snapst)
	c.Assert(err, IsNil)

	c.Assert(snapst.Active, Equals, true)
	c.Assert(snapst.Sequence.Revisions, HasLen, 2)
	c.Assert(snapst.Sequence.Revisions[0], DeepEquals, sequence.NewRevisionSideState(&snap.SideInfo{
		RealName: "some-kernel",
		Channel:  "",
		Revision: snap.R(-42),
	}, currentComponentStates))
	c.Assert(snapst.Sequence.Revisions[1], DeepEquals, sequence.NewRevisionSideState(&snap.SideInfo{
		RealName: "some-kernel",
		Channel:  "channel-for-components",
		SnapID:   "some-kernel-id",
		Revision: snap.R(11),
	}, expectedComponentStates))
}

func (s *snapmgrTestSuite) testUpdateRunThrough(c *C, refreshAppAwarenessUX bool) {
	// we start without the auxiliary store info (or with an older one)
	c.Check(backend.AuxStoreInfoFilename("services-snap-id"), testutil.FileAbsent)

	// use services-snap here to make sure services would be stopped/started appropriately
	si := snap.SideInfo{
		RealName: "services-snap",
		Revision: snap.R(7),
		SnapID:   "services-snap-id",
	}
	snaptest.MockSnap(c, `name: services-snap`, &si)
	fi, err := os.Stat(snap.MountFile("services-snap", si.Revision))
	c.Assert(err, IsNil)
	refreshedDate := fi.ModTime()
	// look at disk
	r := snapstate.MockRevisionDate(nil)
	defer r()

	now, err := time.Parse(time.RFC3339, "2021-06-10T10:00:00Z")
	c.Assert(err, IsNil)
	restoreTimeNow := snapstate.MockTimeNow(func() time.Time {
		return now
	})
	defer restoreTimeNow()

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

	snapstate.Set(s.state, "services-snap", &snapstate.SnapState{
		Active:          true,
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si}),
		Current:         si.Revision,
		SnapType:        "app",
		TrackingChannel: "latest/stable",
		CohortKey:       "embattled",
	})

	chg := s.state.NewChange("refresh", "refresh a snap")
	ts, err := snapstate.Update(s.state, "services-snap", &snapstate.RevisionOptions{
		Channel:   "some-channel",
		CohortKey: "some-cohort",
	}, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)
	chg.AddAll(ts)

	// check unlink-reason
	unlinkTask := findLastTask(chg, "unlink-current-snap")
	c.Assert(unlinkTask, NotNil)
	var unlinkReason string
	unlinkTask.Get("unlink-reason", &unlinkReason)
	c.Check(unlinkReason, Equals, "refresh")

	// local modifications, edge must be set
	te := ts.MaybeEdge(snapstate.LastBeforeLocalModificationsEdge)
	c.Assert(te, NotNil)
	c.Assert(te.Kind(), Equals, "validate-snap")

	s.settle(c)

	expected := fakeOps{
		{
			op: "storesvc-snap-action",
			curSnaps: []store.CurrentSnap{{
				InstanceName:    "services-snap",
				SnapID:          "services-snap-id",
				Revision:        snap.R(7),
				TrackingChannel: "latest/stable",
				RefreshedDate:   refreshedDate,
				Epoch:           snap.E("0"),
				CohortKey:       "embattled",
				Resources:       make(map[string]snap.Revision),
			}},
			userID: 1,
		},
		{
			op: "storesvc-snap-action:action",
			action: store.SnapAction{
				Action:       "refresh",
				InstanceName: "services-snap",
				SnapID:       "services-snap-id",
				Channel:      "some-channel",
				CohortKey:    "some-cohort",
				Flags:        store.SnapActionEnforceValidation,
			},
			revno:  snap.R(11),
			userID: 1,
		},
		{
			op:   "storesvc-download",
			name: "services-snap",
		},
		{
			op:    "validate-snap:Doing",
			name:  "services-snap",
			revno: snap.R(11),
		},
		{
			op:  "current",
			old: filepath.Join(dirs.SnapMountDir, "services-snap/7"),
		},
		{
			op:   "open-snap-file",
			path: filepath.Join(dirs.SnapBlobDir, "services-snap_11.snap"),
			sinfo: snap.SideInfo{
				RealName: "services-snap",
				SnapID:   "services-snap-id",
				Channel:  "some-channel",
				Revision: snap.R(11),
			},
		},
		{
			op:    "setup-snap",
			name:  "services-snap",
			path:  filepath.Join(dirs.SnapBlobDir, "services-snap_11.snap"),
			revno: snap.R(11),
		},
		{
			op: "current-snap-service-states",
		},
		{
			op:       "stop-snap-services:refresh",
			path:     filepath.Join(dirs.SnapMountDir, "services-snap/7"),
			services: []string{"svc1", "svc2", "svc3"},
		},
	}
	// aliases removal is skipped when refresh-app-awareness-ux is enabled
	if !refreshAppAwarenessUX {
		expected = append(expected, fakeOp{
			op:   "remove-snap-aliases",
			name: "services-snap",
		})
	}
	expected = append(expected, fakeOps{
		{
			op:          "run-inhibit-snap-for-unlink",
			name:        "services-snap",
			inhibitHint: "refresh",
		},
		{
			op:   "discard-namespace-locked",
			name: "services-snap",
		},
		{
			op:                 "unlink-snap",
			path:               filepath.Join(dirs.SnapMountDir, "services-snap/7"),
			unlinkSkipBinaries: refreshAppAwarenessUX,
			inhibitHint:        "refresh",
		},
		{
			op:   "copy-data",
			path: filepath.Join(dirs.SnapMountDir, "services-snap/11"),
			old:  filepath.Join(dirs.SnapMountDir, "services-snap/7"),
		},
		{
			op:   "setup-snap-save-data",
			path: filepath.Join(dirs.SnapDataSaveDir, "services-snap"),
		},
		{
			op:    "setup-profiles:Doing",
			name:  "services-snap",
			revno: snap.R(11),
		},
		{
			op: "candidate",
			sinfo: snap.SideInfo{
				RealName: "services-snap",
				SnapID:   "services-snap-id",
				Channel:  "some-channel",
				Revision: snap.R(11),
			},
		},
		{
			op:   "link-snap",
			path: filepath.Join(dirs.SnapMountDir, "services-snap/11"),
		},
		{
			op: "maybe-set-next-boot",
		},
		{
			op:    "auto-connect:Doing",
			name:  "services-snap",
			revno: snap.R(11),
		},
		{
			op: "update-aliases",
		},
		{
			op:       "start-snap-services",
			path:     filepath.Join(dirs.SnapMountDir, "services-snap/11"),
			services: []string{"svc1", "svc3", "svc2"},
		},
		{
			op:    "cleanup-trash",
			name:  "services-snap",
			revno: snap.R(11),
		},
	}...)

	// ensure all our tasks ran
	c.Check(s.fakeStore.downloads, DeepEquals, []fakeDownload{{
		macaroon: s.user.StoreMacaroon,
		name:     "services-snap",
		target:   filepath.Join(dirs.SnapBlobDir, "services-snap_11.snap"),
	}})
	c.Check(s.fakeStore.seenPrivacyKeys["privacy-key"], Equals, true, Commentf("salts seen: %v", s.fakeStore.seenPrivacyKeys))
	// start with an easier-to-read error if this fails:
	c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
	c.Assert(s.fakeBackend.ops, DeepEquals, expected)

	// check progress
	task := ts.Tasks()[1]
	_, cur, total := task.Progress()
	c.Assert(cur, Equals, s.fakeStore.fakeCurrentProgress)
	c.Assert(total, Equals, s.fakeStore.fakeTotalProgress)

	// verify snapSetup info
	var snapsup snapstate.SnapSetup
	err = task.Get("snap-setup", &snapsup)
	c.Assert(err, IsNil)
	c.Assert(snapsup, DeepEquals, snapstate.SnapSetup{
		Channel:   "some-channel",
		CohortKey: "some-cohort",
		UserID:    s.user.ID,

		DownloadInfo: &snap.DownloadInfo{
			DownloadURL: "https://some-server.com/some/path.snap",
			Sha3_384:    "<some-hash>",
		},
		SideInfo:  snapsup.SideInfo,
		Type:      snap.TypeApp,
		Version:   "services-snapVer",
		PlugsOnly: true,
		Flags: snapstate.Flags{
			Transaction: client.TransactionPerSnap,
		},
		PreUpdateKernelModuleComponents: []*snap.ComponentSideInfo{},
	})
	c.Assert(snapsup.SideInfo, DeepEquals, &snap.SideInfo{
		RealName: "services-snap",
		Revision: snap.R(11),
		Channel:  "some-channel",
		SnapID:   "services-snap-id",
	})

	// verify services stop reason
	verifyStopReason(c, ts, "refresh")

	// check post-refresh hook
	var postRefreshHookTask *state.Task
	for _, t := range ts.Tasks() {
		if t.Kind() != "run-hook" {
			continue
		}
		if strings.Contains(t.Summary(), `Run post-refresh hook of "services-snap" snap if present`) {
			postRefreshHookTask = t
			break
		}
	}
	c.Assert(postRefreshHookTask, NotNil, Commentf(printTasks(ts.Tasks())))

	// verify snaps in the system state
	var snapst snapstate.SnapState
	err = snapstate.Get(s.state, "services-snap", &snapst)
	c.Assert(err, IsNil)

	c.Assert(snapst.LastRefreshTime, NotNil)
	c.Check(snapst.LastRefreshTime.Equal(now), Equals, true)
	c.Assert(snapst.Active, Equals, true)
	c.Assert(snapst.Sequence.Revisions, HasLen, 2)
	c.Assert(snapst.Sequence.Revisions[0], DeepEquals, sequence.NewRevisionSideState(&snap.SideInfo{
		RealName: "services-snap",
		SnapID:   "services-snap-id",
		Channel:  "",
		Revision: snap.R(7),
	}, nil))
	c.Assert(snapst.Sequence.Revisions[1], DeepEquals, sequence.NewRevisionSideState(&snap.SideInfo{
		RealName: "services-snap",
		Channel:  "some-channel",
		SnapID:   "services-snap-id",
		Revision: snap.R(11),
	}, nil))
	c.Check(snapst.CohortKey, Equals, "some-cohort")

	// we end up with the auxiliary store info
	c.Check(backend.AuxStoreInfoFilename("services-snap-id"), testutil.FilePresent)
}

func (s *snapmgrTestSuite) TestUpdateRunThrough(c *C) {
	s.testUpdateRunThrough(c, false)
}

func (s *snapmgrTestSuite) TestUpdateRunThroughSkipBinaries(c *C) {
	s.enableRefreshAppAwarenessUX()
	s.testUpdateRunThrough(c, true)
}

func (s *snapmgrTestSuite) TestUpdateDropsRevertStatus(c *C) {
	si := snap.SideInfo{
		RealName: "services-snap",
		Revision: snap.R(7),
		SnapID:   "services-snap-id",
	}
	snaptest.MockSnap(c, `name: services-snap`, &si)

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

	si2 := snap.SideInfo{
		RealName: "services-snap",
		Revision: snap.R(11),
		SnapID:   "services-snap-id",
	}
	snapstate.Set(s.state, "services-snap", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si, &si2}),
		Current:  si.Revision,
		RevertStatus: map[int]snapstate.RevertStatus{
			11: snapstate.NotBlocked,
		},
		SnapType:        "app",
		TrackingChannel: "latest/stable",
		CohortKey:       "embattled",
	})

	chg := s.state.NewChange("refresh", "refresh a snap")
	ts, err := snapstate.Update(s.state, "services-snap", &snapstate.RevisionOptions{
		Channel:   "some-channel",
		CohortKey: "some-cohort",
	}, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)
	chg.AddAll(ts)

	s.settle(c)

	// verify snaps in the system state
	var snapst snapstate.SnapState
	c.Assert(snapstate.Get(s.state, "services-snap", &snapst), IsNil)
	c.Assert(snapst.Active, Equals, true)
	c.Assert(snapst.Current, Equals, snap.R(11))
	c.Assert(snapst.Sequence.Revisions, HasLen, 2)
	c.Assert(snapst.Sequence.Revisions[0], DeepEquals, sequence.NewRevisionSideState(&snap.SideInfo{
		RealName: "services-snap",
		SnapID:   "services-snap-id",
		Channel:  "",
		Revision: snap.R(7),
	}, nil))
	c.Assert(snapst.Sequence.Revisions[1], DeepEquals, sequence.NewRevisionSideState(&snap.SideInfo{
		RealName: "services-snap",
		Channel:  "some-channel",
		SnapID:   "services-snap-id",
		Revision: snap.R(11),
	}, nil))
	c.Check(snapst.RevertStatus, HasLen, 0)
}

func (s *snapmgrTestSuite) TestUpdateResetsHoldState(c *C) {
	si := snap.SideInfo{
		RealName: "some-snap",
		Revision: snap.R(7),
		SnapID:   "some-snap-id",
	}
	snaptest.MockSnap(c, `name: some-snap`, &si)

	si2 := snap.SideInfo{
		RealName: "other-snap",
		Revision: snap.R(7),
		SnapID:   "other-snap-id",
	}
	snaptest.MockSnap(c, `name: other-snap`, &si2)

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

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:          true,
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si}),
		Current:         si.Revision,
		SnapType:        "app",
		TrackingChannel: "latest/stable",
	})

	snapstate.Set(s.state, "other-snap", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si2}),
		Current:  si.Revision,
		SnapType: "app",
	})

	// enable gate-auto-refresh-hook feature
	tr := config.NewTransaction(s.state)
	tr.Set("core", "experimental.gate-auto-refresh-hook", true)
	tr.Commit()

	// pretend that the snap was held during last auto-refresh
	_, err := snapstate.HoldRefresh(s.state, snapstate.HoldAutoRefresh, "gating-snap", 0, "some-snap", "other-snap")
	c.Assert(err, IsNil)
	// validity check
	held, err := snapstate.HeldSnaps(s.state, snapstate.HoldAutoRefresh)
	c.Assert(err, IsNil)
	c.Check(held, DeepEquals, map[string][]string{
		"some-snap":  {"gating-snap"},
		"other-snap": {"gating-snap"},
	})

	_, err = snapstate.Update(s.state, "some-snap", nil, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)

	// and it is not held anymore (but other-snap still is)
	held, err = snapstate.HeldSnaps(s.state, snapstate.HoldAutoRefresh)
	c.Assert(err, IsNil)
	c.Check(held, DeepEquals, map[string][]string{
		"other-snap": {"gating-snap"},
	})
}

func (s *snapmgrTestSuite) testParallelInstanceUpdateRunThrough(c *C, refreshAppAwarenessUX bool) {
	// use services-snap here to make sure services would be stopped/started appropriately
	si := snap.SideInfo{
		RealName: "services-snap",
		Revision: snap.R(7),
		SnapID:   "services-snap-id",
	}
	snaptest.MockSnapInstance(c, "services-snap_instance", `name: services-snap`, &si)
	fi, err := os.Stat(snap.MountFile("services-snap_instance", si.Revision))
	c.Assert(err, IsNil)
	refreshedDate := fi.ModTime()
	// look at disk
	r := snapstate.MockRevisionDate(nil)
	defer r()

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

	tr := config.NewTransaction(s.state)
	tr.Set("core", "experimental.parallel-instances", true)
	tr.Commit()

	snapstate.Set(s.state, "services-snap_instance", &snapstate.SnapState{
		Active:          true,
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si}),
		Current:         si.Revision,
		SnapType:        "app",
		TrackingChannel: "latest/stable",
		InstanceKey:     "instance",
	})

	chg := s.state.NewChange("refresh", "refresh a snap")
	ts, err := snapstate.Update(s.state, "services-snap_instance", &snapstate.RevisionOptions{Channel: "some-channel"}, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)
	chg.AddAll(ts)

	s.settle(c)

	expected := fakeOps{
		{
			op: "storesvc-snap-action",
			curSnaps: []store.CurrentSnap{{
				InstanceName:    "services-snap_instance",
				SnapID:          "services-snap-id",
				Revision:        snap.R(7),
				TrackingChannel: "latest/stable",
				RefreshedDate:   refreshedDate,
				Epoch:           snap.E("0"),
				Resources:       make(map[string]snap.Revision),
			}},
			userID: 1,
		},
		{
			op: "storesvc-snap-action:action",
			action: store.SnapAction{
				Action:       "refresh",
				SnapID:       "services-snap-id",
				InstanceName: "services-snap_instance",
				Channel:      "some-channel",
				Flags:        store.SnapActionEnforceValidation,
			},
			revno:  snap.R(11),
			userID: 1,
		},
		{
			op:   "storesvc-download",
			name: "services-snap",
		},
		{
			op:    "validate-snap:Doing",
			name:  "services-snap_instance",
			revno: snap.R(11),
		},
		{
			op:  "current",
			old: filepath.Join(dirs.SnapMountDir, "services-snap_instance/7"),
		},
		{
			op:   "open-snap-file",
			path: filepath.Join(dirs.SnapBlobDir, "services-snap_instance_11.snap"),
			sinfo: snap.SideInfo{
				RealName: "services-snap",
				SnapID:   "services-snap-id",
				Channel:  "some-channel",
				Revision: snap.R(11),
			},
		},
		{
			op:    "setup-snap",
			name:  "services-snap_instance",
			path:  filepath.Join(dirs.SnapBlobDir, "services-snap_instance_11.snap"),
			revno: snap.R(11),
		},
		{
			op: "current-snap-service-states",
		},
		{
			op:       "stop-snap-services:refresh",
			path:     filepath.Join(dirs.SnapMountDir, "services-snap_instance/7"),
			services: []string{"svc1", "svc2", "svc3"},
		},
	}
	// aliases removal is skipped when refresh-app-awareness-ux is enabled
	if !refreshAppAwarenessUX {
		expected = append(expected, fakeOp{
			op:   "remove-snap-aliases",
			name: "services-snap_instance",
		})
	}
	expected = append(expected, fakeOps{
		{
			op:          "run-inhibit-snap-for-unlink",
			name:        "services-snap_instance",
			inhibitHint: "refresh",
		},
		{
			op:   "discard-namespace-locked",
			name: "services-snap_instance",
		},
		{
			op:                 "unlink-snap",
			path:               filepath.Join(dirs.SnapMountDir, "services-snap_instance/7"),
			unlinkSkipBinaries: refreshAppAwarenessUX,
			inhibitHint:        "refresh",
		},
		{
			op:   "copy-data",
			path: filepath.Join(dirs.SnapMountDir, "services-snap_instance/11"),
			old:  filepath.Join(dirs.SnapMountDir, "services-snap_instance/7"),
		},
		{
			op:   "setup-snap-save-data",
			path: filepath.Join(dirs.SnapDataSaveDir, "services-snap_instance"),
		},
		{
			op:    "setup-profiles:Doing",
			name:  "services-snap_instance",
			revno: snap.R(11),
		},
		{
			op: "candidate",
			sinfo: snap.SideInfo{
				RealName: "services-snap",
				SnapID:   "services-snap-id",
				Channel:  "some-channel",
				Revision: snap.R(11),
			},
		},
		{
			op:   "link-snap",
			path: filepath.Join(dirs.SnapMountDir, "services-snap_instance/11"),
		},
		{
			op: "maybe-set-next-boot",
		},
		{
			op:    "auto-connect:Doing",
			name:  "services-snap_instance",
			revno: snap.R(11),
		},
		{
			op: "update-aliases",
		},
		{
			op:       "start-snap-services",
			path:     filepath.Join(dirs.SnapMountDir, "services-snap_instance/11"),
			services: []string{"svc1", "svc3", "svc2"},
		},
		{
			op:    "cleanup-trash",
			name:  "services-snap_instance",
			revno: snap.R(11),
		},
	}...)

	// ensure all our tasks ran
	c.Check(s.fakeStore.downloads, DeepEquals, []fakeDownload{{
		macaroon: s.user.StoreMacaroon,
		name:     "services-snap",
		target:   filepath.Join(dirs.SnapBlobDir, "services-snap_instance_11.snap"),
	}})
	c.Check(s.fakeStore.seenPrivacyKeys["privacy-key"], Equals, true, Commentf("salts seen: %v", s.fakeStore.seenPrivacyKeys))
	// start with an easier-to-read error if this fails:
	c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
	c.Assert(s.fakeBackend.ops, DeepEquals, expected)

	// check progress
	task := ts.Tasks()[1]
	_, cur, total := task.Progress()
	c.Assert(cur, Equals, s.fakeStore.fakeCurrentProgress)
	c.Assert(total, Equals, s.fakeStore.fakeTotalProgress)

	// verify snapSetup info
	var snapsup snapstate.SnapSetup
	err = task.Get("snap-setup", &snapsup)
	c.Assert(err, IsNil)
	c.Assert(snapsup, DeepEquals, snapstate.SnapSetup{
		Channel: "some-channel",
		UserID:  s.user.ID,

		DownloadInfo: &snap.DownloadInfo{
			DownloadURL: "https://some-server.com/some/path.snap",
			Sha3_384:    "<some-hash>",
		},
		SideInfo:    snapsup.SideInfo,
		Type:        snap.TypeApp,
		Version:     "services-snapVer",
		PlugsOnly:   true,
		InstanceKey: "instance",
		Flags: snapstate.Flags{
			Transaction: client.TransactionPerSnap,
		},
		PreUpdateKernelModuleComponents: []*snap.ComponentSideInfo{},
	})
	c.Assert(snapsup.SideInfo, DeepEquals, &snap.SideInfo{
		RealName: "services-snap",
		Revision: snap.R(11),
		Channel:  "some-channel",
		SnapID:   "services-snap-id",
	})

	// verify services stop reason
	verifyStopReason(c, ts, "refresh")

	// check post-refresh hook
	var postRefreshHookTask *state.Task
	for _, t := range ts.Tasks() {
		if t.Kind() == "run-hook" && strings.Contains(t.Summary(), `Run post-refresh hook of "services-snap_instance" snap`) {
			postRefreshHookTask = t
			break
		}
	}
	c.Assert(postRefreshHookTask, NotNil)
	c.Assert(postRefreshHookTask.Summary(), Matches, `Run post-refresh hook of "services-snap_instance" snap if present`)

	// verify snaps in the system state
	var snapst snapstate.SnapState
	err = snapstate.Get(s.state, "services-snap_instance", &snapst)
	c.Assert(err, IsNil)

	c.Assert(snapst.InstanceKey, Equals, "instance")
	c.Assert(snapst.Active, Equals, true)
	c.Assert(snapst.Sequence.Revisions, HasLen, 2)
	c.Assert(snapst.Sequence.Revisions[0], DeepEquals, sequence.NewRevisionSideState(&snap.SideInfo{
		RealName: "services-snap",
		SnapID:   "services-snap-id",
		Channel:  "",
		Revision: snap.R(7),
	}, nil))
	c.Assert(snapst.Sequence.Revisions[1], DeepEquals, sequence.NewRevisionSideState(&snap.SideInfo{
		RealName: "services-snap",
		Channel:  "some-channel",
		SnapID:   "services-snap-id",
		Revision: snap.R(11),
	}, nil))
}

func (s *snapmgrTestSuite) TestParallelInstanceUpdateRunThrough(c *C) {
	s.testParallelInstanceUpdateRunThrough(c, false)
}

func (s *snapmgrTestSuite) TestParallelInstanceUpdateRunThroughSkipBinaries(c *C) {
	s.enableRefreshAppAwarenessUX()
	s.testParallelInstanceUpdateRunThrough(c, true)
}

func (s *snapmgrTestSuite) TestUpdateWithNewBase(c *C) {
	si := &snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Revision: snap.R(7),
	}
	snaptest.MockSnap(c, `name: some-snap`, si)

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

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:          true,
		TrackingChannel: "latest/edge",
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
		Current:         snap.R(7),
		SnapType:        "app",
	})

	chg := s.state.NewChange("refresh", "refresh a snap")
	ts, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "channel-for-base/stable"}, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)
	chg.AddAll(ts)

	s.settle(c)

	c.Check(s.fakeStore.downloads, DeepEquals, []fakeDownload{
		{macaroon: s.user.StoreMacaroon, name: "some-base", target: filepath.Join(dirs.SnapBlobDir, "some-base_11.snap")},
		{macaroon: s.user.StoreMacaroon, name: "some-snap", target: filepath.Join(dirs.SnapBlobDir, "some-snap_11.snap")},
	})
}

func (s *snapmgrTestSuite) TestUpdateWithAlreadyInstalledBase(c *C) {
	si := &snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Revision: snap.R(7),
	}
	snaptest.MockSnap(c, `name: some-snap`, si)

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

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:          true,
		TrackingChannel: "latest/edge",
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
		Current:         snap.R(7),
		SnapType:        "app",
	})
	snapstate.Set(s.state, "some-base", &snapstate.SnapState{
		Active:          true,
		TrackingChannel: "latest/stable",
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{
			RealName: "some-base",
			SnapID:   "some-base-id",
			Revision: snap.R(1),
		}}),
		Current:  snap.R(1),
		SnapType: "base",
	})

	chg := s.state.NewChange("refresh", "refresh a snap")
	ts, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "channel-for-base"}, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)
	chg.AddAll(ts)

	s.settle(c)

	c.Check(s.fakeStore.downloads, DeepEquals, []fakeDownload{
		{macaroon: s.user.StoreMacaroon, name: "some-snap", target: filepath.Join(dirs.SnapBlobDir, "some-snap_11.snap")},
	})
}

func (s *snapmgrTestSuite) TestUpdateWithNewDefaultProvider(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	snapstate.ReplaceStore(s.state, contentStore{fakeStore: s.fakeStore, state: s.state})
	repo := interfaces.NewRepository()
	ifacerepo.Replace(s.state, repo)

	si := &snap.SideInfo{
		RealName: "snap-content-plug",
		SnapID:   "snap-content-plug-id",
		Revision: snap.R(7),
	}
	snaptest.MockSnap(c, `name: snap-content-plug`, si)
	snapstate.Set(s.state, "snap-content-plug", &snapstate.SnapState{
		Active:          true,
		TrackingChannel: "latest/edge",
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
		Current:         snap.R(7),
		SnapType:        "app",
	})

	chg := s.state.NewChange("refresh", "refresh a snap")
	ts, err := snapstate.Update(s.state, "snap-content-plug", &snapstate.RevisionOptions{Channel: "stable"}, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)
	chg.AddAll(ts)

	s.settle(c)

	c.Check(s.fakeStore.downloads, DeepEquals, []fakeDownload{
		{macaroon: s.user.StoreMacaroon, name: "snap-content-plug", target: filepath.Join(dirs.SnapBlobDir, "snap-content-plug_11.snap")},
		{macaroon: s.user.StoreMacaroon, name: "snap-content-slot", target: filepath.Join(dirs.SnapBlobDir, "snap-content-slot_11.snap")},
	})
}

func (s *snapmgrTestSuite) TestUpdateWithInstalledDefaultProvider(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	snapstate.ReplaceStore(s.state, contentStore{fakeStore: s.fakeStore, state: s.state})
	repo := interfaces.NewRepository()
	ifacerepo.Replace(s.state, repo)

	si := &snap.SideInfo{
		RealName: "snap-content-plug",
		SnapID:   "snap-content-plug-id",
		Revision: snap.R(7),
	}
	snaptest.MockSnap(c, `name: snap-content-plug`, si)
	snapstate.Set(s.state, "snap-content-plug", &snapstate.SnapState{
		Active:          true,
		TrackingChannel: "latest/edge",
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
		Current:         snap.R(7),
		SnapType:        "app",
	})
	snapstate.Set(s.state, "snap-content-slot", &snapstate.SnapState{
		Active:          true,
		TrackingChannel: "latest/stable",
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{
			RealName: "snap-content-slot",
			SnapID:   "snap-content-slot-id",
			Revision: snap.R(1),
		}}),
		Current:  snap.R(1),
		SnapType: "app",
	})

	err := repo.AddInterface(&ifacetest.TestInterface{InterfaceName: "content"})
	c.Assert(err, IsNil)

	sn := &snap.Info{SuggestedName: "snap-content-slot", Slots: make(map[string]*snap.SlotInfo), Version: "1"}
	slot := &snap.SlotInfo{
		Snap:      sn,
		Name:      "snap-content-slot",
		Interface: "content",
		Attrs: map[string]any{
			"content": "shared-content",
		},
	}
	sn.Slots["snap-content-slot"] = slot

	appSet, err := interfaces.NewSnapAppSet(sn, nil)
	c.Assert(err, IsNil)

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

	chg := s.state.NewChange("refresh", "refresh a snap")
	ts, err := snapstate.Update(s.state, "snap-content-plug", &snapstate.RevisionOptions{Channel: "stable"}, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)
	chg.AddAll(ts)

	s.settle(c)

	c.Check(s.fakeStore.downloads, DeepEquals, []fakeDownload{
		{macaroon: s.user.StoreMacaroon, name: "snap-content-plug", target: filepath.Join(dirs.SnapBlobDir, "snap-content-plug_11.snap")},
	})
}

func (s *snapmgrTestSuite) TestUpdateRememberedUserRunThrough(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-snap", Revision: snap.R(5), SnapID: "some-snap-id"},
		}),
		Current:  snap.R(5),
		SnapType: "app",
		UserID:   1,
	})

	chg := s.state.NewChange("refresh", "refresh a snap")
	ts, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "some-channel"}, 0, snapstate.Flags{})
	c.Assert(err, IsNil)
	chg.AddAll(ts)

	s.settle(c)

	c.Assert(chg.Status(), Equals, state.DoneStatus)
	c.Assert(chg.Err(), IsNil)

	for _, op := range s.fakeBackend.ops {
		switch op.op {
		case "storesvc-snap-action":
			c.Check(op.userID, Equals, 1)
		case "storesvc-download":
			snapName := op.name
			c.Check(s.fakeStore.downloads[0], DeepEquals, fakeDownload{
				macaroon: "macaroon",
				name:     "some-snap",
				target:   filepath.Join(dirs.SnapBlobDir, "some-snap_11.snap"),
			}, Commentf(snapName))
		}
	}
}

func (s *snapmgrTestSuite) TestUpdateModelKernelSwitchTrackRunThrough(c *C) {
	restore := release.MockOnClassic(false)
	defer restore()

	// use services-snap here to make sure services would be stopped/started appropriately
	si := snap.SideInfo{
		RealName: "kernel",
		Revision: snap.R(7),
		SnapID:   "kernel-id",
	}
	snaptest.MockSnap(c, `name: kernel`, &si)
	fi, err := os.Stat(snap.MountFile("kernel", si.Revision))
	c.Assert(err, IsNil)
	refreshedDate := fi.ModTime()
	// look at disk
	r := snapstate.MockRevisionDate(nil)
	defer r()

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

	r1 := snapstatetest.MockDeviceModel(ModelWithKernelTrack("18"))
	defer r1()
	snapstate.Set(s.state, "kernel", &snapstate.SnapState{
		Active:          true,
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si}),
		Current:         si.Revision,
		TrackingChannel: "18/stable",
	})

	chg := s.state.NewChange("refresh", "refresh a snap")
	ts, err := snapstate.Update(s.state, "kernel", &snapstate.RevisionOptions{Channel: "edge"}, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)
	chg.AddAll(ts)

	s.settle(c)

	c.Check(chg.Status(), Equals, state.DoneStatus)

	c.Assert(len(s.fakeBackend.ops) > 2, Equals, true)
	c.Assert(s.fakeBackend.ops[:2], DeepEquals, fakeOps{
		{
			op: "storesvc-snap-action",
			curSnaps: []store.CurrentSnap{{
				InstanceName:    "kernel",
				SnapID:          "kernel-id",
				Revision:        snap.R(7),
				TrackingChannel: "18/stable",
				RefreshedDate:   refreshedDate,
				Epoch:           snap.E("1*"),
				Resources:       make(map[string]snap.Revision),
			}},
			userID: 1,
		}, {
			op: "storesvc-snap-action:action",
			action: store.SnapAction{
				Action:       "refresh",
				InstanceName: "kernel",
				SnapID:       "kernel-id",
				Channel:      "18/edge",
				Flags:        store.SnapActionEnforceValidation,
			},
			revno:  snap.R(11),
			userID: 1,
		},
	})

	// check progress
	task := ts.Tasks()[1]
	_, cur, total := task.Progress()
	c.Assert(cur, Equals, s.fakeStore.fakeCurrentProgress)
	c.Assert(total, Equals, s.fakeStore.fakeTotalProgress)

	// verify snapSetup info
	var snapsup snapstate.SnapSetup
	err = task.Get("snap-setup", &snapsup)
	c.Assert(err, IsNil)
	c.Assert(snapsup, DeepEquals, snapstate.SnapSetup{
		Channel: "18/edge",
		UserID:  s.user.ID,

		DownloadInfo: &snap.DownloadInfo{
			DownloadURL: "https://some-server.com/some/path.snap",
			Sha3_384:    "<some-hash>",
		},
		SideInfo:  snapsup.SideInfo,
		Type:      snap.TypeKernel,
		Version:   "kernelVer",
		PlugsOnly: true,
		Flags: snapstate.Flags{
			Transaction: client.TransactionPerSnap,
		},
		PreUpdateKernelModuleComponents: []*snap.ComponentSideInfo{},
	})
	c.Assert(snapsup.SideInfo, DeepEquals, &snap.SideInfo{
		RealName: "kernel",
		Revision: snap.R(11),
		Channel:  "18/edge",
		SnapID:   "kernel-id",
	})

	// verify snaps in the system state
	var snapst snapstate.SnapState
	err = snapstate.Get(s.state, "kernel", &snapst)
	c.Assert(err, IsNil)

	c.Assert(snapst.Active, Equals, true)
	c.Assert(snapst.TrackingChannel, Equals, "18/edge")
	c.Assert(snapst.Sequence.Revisions, HasLen, 2)
	c.Assert(snapst.Sequence.Revisions[0], DeepEquals, sequence.NewRevisionSideState(&snap.SideInfo{
		RealName: "kernel",
		SnapID:   "kernel-id",
		Channel:  "",
		Revision: snap.R(7),
	}, nil))
	c.Assert(snapst.Sequence.Revisions[1], DeepEquals, sequence.NewRevisionSideState(&snap.SideInfo{
		RealName: "kernel",
		Channel:  "18/edge",
		SnapID:   "kernel-id",
		Revision: snap.R(11),
	}, nil))
}

func (s *snapmgrTestSuite) TestUpdateManyMultipleCredsNoUserRunThrough(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	snapstate.Set(s.state, "core", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "core", Revision: snap.R(1), SnapID: "core-snap-id"},
		}),
		Current:  snap.R(1),
		SnapType: "os",
	})
	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-snap", Revision: snap.R(5), SnapID: "some-snap-id"},
		}),
		Current:  snap.R(5),
		SnapType: "app",
		UserID:   1,
	})
	snapstate.Set(s.state, "services-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "services-snap", Revision: snap.R(2), SnapID: "services-snap-id"},
		}),
		Current:  snap.R(2),
		SnapType: "app",
		UserID:   2,
	})

	chg := s.state.NewChange("refresh", "refresh all snaps")
	// no user is passed to use for UpdateMany
	updated, tts, err := snapstate.UpdateMany(context.Background(), s.state, nil, nil, 0, nil)
	c.Assert(err, IsNil)
	for _, ts := range tts {
		chg.AddAll(ts)
	}
	c.Check(updated, HasLen, 3)

	s.settle(c)

	c.Assert(chg.Status(), Equals, state.DoneStatus)
	c.Assert(chg.Err(), IsNil)

	macaroonMap := map[string]string{
		"core":          "",
		"some-snap":     "macaroon",
		"services-snap": "macaroon2",
	}

	seen := make(map[string]int)
	ir := 0
	di := 0
	for _, op := range s.fakeBackend.ops {
		switch op.op {
		case "storesvc-snap-action":
			ir++
			c.Check(op.curSnaps, DeepEquals, []store.CurrentSnap{
				{
					InstanceName:  "core",
					SnapID:        "core-snap-id",
					Revision:      snap.R(1),
					RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 1),
					Epoch:         snap.E("1*"),
					Resources:     make(map[string]snap.Revision),
				},
				{
					InstanceName:  "services-snap",
					SnapID:        "services-snap-id",
					Revision:      snap.R(2),
					RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 2),
					Epoch:         snap.E("0"),
					Resources:     make(map[string]snap.Revision),
				},
				{
					InstanceName:  "some-snap",
					SnapID:        "some-snap-id",
					Revision:      snap.R(5),
					RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 5),
					Epoch:         snap.E("1*"),
					Resources:     make(map[string]snap.Revision),
				},
			})
		case "storesvc-snap-action:action":
			snapID := op.action.SnapID
			seen[snapID] = op.userID
		case "storesvc-download":
			snapName := op.name
			fakeDl := s.fakeStore.downloads[di]
			// check target path separately and clear it
			c.Check(fakeDl.target, Matches, filepath.Join(dirs.SnapBlobDir, fmt.Sprintf("%s_[0-9]+.snap", snapName)))
			fakeDl.target = ""
			c.Check(fakeDl, DeepEquals, fakeDownload{
				macaroon: macaroonMap[snapName],
				name:     snapName,
			}, Commentf(snapName))
			di++
		}
	}
	c.Check(ir, Equals, 2)
	// we check all snaps with each user
	c.Check(seen["some-snap-id"], Equals, 1)
	c.Check(seen["services-snap-id"], Equals, 2)
	// coalesced with one of the others
	c.Check(seen["core-snap-id"] > 0, Equals, true)
}

func (s *snapmgrTestSuite) TestUpdateManyMultipleCredsUserRunThrough(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	snapstate.Set(s.state, "core", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "core", Revision: snap.R(1), SnapID: "core-snap-id"},
		}),
		Current:  snap.R(1),
		SnapType: "os",
	})
	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-snap", Revision: snap.R(5), SnapID: "some-snap-id"},
		}),
		Current:  snap.R(5),
		SnapType: "app",
		UserID:   1,
	})
	snapstate.Set(s.state, "services-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "services-snap", Revision: snap.R(2), SnapID: "services-snap-id"},
		}),
		Current:  snap.R(2),
		SnapType: "app",
		UserID:   2,
	})

	chg := s.state.NewChange("refresh", "refresh all snaps")
	// do UpdateMany using user 2 as fallback
	updated, tts, err := snapstate.UpdateMany(context.Background(), s.state, nil, nil, 2, nil)
	c.Assert(err, IsNil)
	for _, ts := range tts {
		chg.AddAll(ts)
	}
	c.Check(updated, HasLen, 3)

	s.settle(c)

	c.Assert(chg.Status(), Equals, state.DoneStatus)
	c.Assert(chg.Err(), IsNil)

	macaroonMap := map[string]string{
		"core":          "macaroon2",
		"some-snap":     "macaroon",
		"services-snap": "macaroon2",
	}

	type snapIDuserID struct {
		snapID string
		userID int
	}
	seen := make(map[snapIDuserID]bool)
	ir := 0
	di := 0
	for _, op := range s.fakeBackend.ops {
		switch op.op {
		case "storesvc-snap-action":
			ir++
			c.Check(op.curSnaps, DeepEquals, []store.CurrentSnap{
				{
					InstanceName:  "core",
					SnapID:        "core-snap-id",
					Revision:      snap.R(1),
					RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 1),
					Epoch:         snap.E("1*"),
					Resources:     make(map[string]snap.Revision),
				},
				{
					InstanceName:  "services-snap",
					SnapID:        "services-snap-id",
					Revision:      snap.R(2),
					RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 2),
					Epoch:         snap.E("0"),
					Resources:     make(map[string]snap.Revision),
				},
				{
					InstanceName:  "some-snap",
					SnapID:        "some-snap-id",
					Revision:      snap.R(5),
					RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 5),
					Epoch:         snap.E("1*"),
					Resources:     make(map[string]snap.Revision),
				},
			})
		case "storesvc-snap-action:action":
			snapID := op.action.SnapID
			seen[snapIDuserID{snapID: snapID, userID: op.userID}] = true
		case "storesvc-download":
			snapName := op.name
			fakeDl := s.fakeStore.downloads[di]
			// check target path separately and clear it
			c.Check(fakeDl.target, Matches, filepath.Join(dirs.SnapBlobDir, fmt.Sprintf("%s_[0-9]+.snap", snapName)))
			fakeDl.target = ""
			c.Check(fakeDl, DeepEquals, fakeDownload{
				macaroon: macaroonMap[snapName],
				name:     snapName,
			}, Commentf(snapName))
			di++
		}
	}
	c.Check(ir, Equals, 2)
	// we check all snaps with each user
	c.Check(seen, DeepEquals, map[snapIDuserID]bool{
		{snapID: "core-snap-id", userID: 2}:     true,
		{snapID: "some-snap-id", userID: 1}:     true,
		{snapID: "services-snap-id", userID: 2}: true,
	})

	var coreState, snapState snapstate.SnapState
	// user in SnapState was preserved
	err = snapstate.Get(s.state, "some-snap", &snapState)
	c.Assert(err, IsNil)
	c.Check(snapState.UserID, Equals, 1)
	c.Check(snapState.Current, DeepEquals, snap.R(11))

	// user in SnapState was set
	err = snapstate.Get(s.state, "core", &coreState)
	c.Assert(err, IsNil)
	c.Check(coreState.UserID, Equals, 2)
	c.Check(coreState.Current, DeepEquals, snap.R(11))

}

func (s *snapmgrTestSuite) TestUpdateManyMultipleCredsUserWithNoStoreAuthRunThrough(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	snapstate.Set(s.state, "core", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "core", Revision: snap.R(1), SnapID: "core-snap-id"},
		}),
		Current:  snap.R(1),
		SnapType: "os",
	})
	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-snap", Revision: snap.R(5), SnapID: "some-snap-id"},
		}),
		Current:  snap.R(5),
		SnapType: "app",
		UserID:   1,
	})
	snapstate.Set(s.state, "services-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "services-snap", Revision: snap.R(2), SnapID: "services-snap-id"},
		}),
		Current:  snap.R(2),
		SnapType: "app",
		UserID:   3,
	})

	chg := s.state.NewChange("refresh", "refresh all snaps")
	// no user is passed to use for UpdateMany
	updated, tts, err := snapstate.UpdateMany(context.Background(), s.state, nil, nil, 0, nil)
	c.Assert(err, IsNil)
	for _, ts := range tts {
		chg.AddAll(ts)
	}
	c.Check(updated, HasLen, 3)

	s.settle(c)

	c.Assert(chg.Status(), Equals, state.DoneStatus)
	c.Assert(chg.Err(), IsNil)

	macaroonMap := map[string]string{
		"core":          "",
		"some-snap":     "macaroon",
		"services-snap": "",
	}

	seen := make(map[string]int)
	ir := 0
	di := 0
	for _, op := range s.fakeBackend.ops {
		switch op.op {
		case "storesvc-snap-action":
			ir++
			c.Check(op.curSnaps, DeepEquals, []store.CurrentSnap{
				{
					InstanceName:  "core",
					SnapID:        "core-snap-id",
					Revision:      snap.R(1),
					RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 1),
					Epoch:         snap.E("1*"),
					Resources:     make(map[string]snap.Revision),
				},
				{
					InstanceName:  "services-snap",
					SnapID:        "services-snap-id",
					Revision:      snap.R(2),
					RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 2),
					Epoch:         snap.E("0"),
					Resources:     make(map[string]snap.Revision),
				},
				{
					InstanceName:  "some-snap",
					SnapID:        "some-snap-id",
					Revision:      snap.R(5),
					RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 5),
					Epoch:         snap.E("1*"),
					Resources:     make(map[string]snap.Revision),
				},
			})
		case "storesvc-snap-action:action":
			snapID := op.action.SnapID
			if _, ok := seen[snapID]; !ok {
				seen[snapID] = op.userID
			}
		case "storesvc-download":
			snapName := op.name
			fakeDl := s.fakeStore.downloads[di]
			// check target path separately and clear it
			c.Check(fakeDl.target, Matches, filepath.Join(dirs.SnapBlobDir, fmt.Sprintf("%s_[0-9]+.snap", snapName)))
			fakeDl.target = ""
			c.Check(fakeDl, DeepEquals, fakeDownload{
				macaroon: macaroonMap[snapName],
				name:     snapName,
			}, Commentf(snapName))
			di++
		}
	}
	c.Check(ir, Equals, 1)
	// we check all snaps with each user
	c.Check(seen["some-snap-id"], Equals, 1)
	// coalesced with request for 1
	c.Check(seen["services-snap-id"], Equals, 1)
	c.Check(seen["core-snap-id"], Equals, 1)
}

func (s *snapmgrTestSuite) testUpdateUndoRunThrough(c *C, refreshAppAwarenessUX bool) {
	si := snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Revision: snap.R(7),
	}

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

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si}),
		Current:  si.Revision,
		SnapType: "app",
	})

	chg := s.state.NewChange("install", "install a snap")
	ts, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "some-channel"}, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)
	chg.AddAll(ts)

	s.fakeBackend.linkSnapFailTrigger = filepath.Join(dirs.SnapMountDir, "/some-snap/11")

	s.settle(c)

	expected := fakeOps{
		{
			op: "storesvc-snap-action",
			curSnaps: []store.CurrentSnap{{
				InstanceName:  "some-snap",
				SnapID:        "some-snap-id",
				Revision:      snap.R(7),
				RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 7),
				Epoch:         snap.E("1*"),
				Resources:     make(map[string]snap.Revision),
			}},
			userID: 1,
		},
		{
			op: "storesvc-snap-action:action",
			action: store.SnapAction{
				Action:       "refresh",
				InstanceName: "some-snap",
				SnapID:       "some-snap-id",
				Channel:      "some-channel",
				Flags:        store.SnapActionEnforceValidation,
			},
			revno:  snap.R(11),
			userID: 1,
		},
		{
			op:   "storesvc-download",
			name: "some-snap",
		},
		{
			op:    "validate-snap:Doing",
			name:  "some-snap",
			revno: snap.R(11),
		},
		{
			op:  "current",
			old: filepath.Join(dirs.SnapMountDir, "some-snap/7"),
		},
		{
			op:   "open-snap-file",
			path: filepath.Join(dirs.SnapBlobDir, "some-snap_11.snap"),
			sinfo: snap.SideInfo{
				RealName: "some-snap",
				SnapID:   "some-snap-id",
				Channel:  "some-channel",
				Revision: snap.R(11),
			},
		},
		{
			op:    "setup-snap",
			name:  "some-snap",
			path:  filepath.Join(dirs.SnapBlobDir, "some-snap_11.snap"),
			revno: snap.R(11),
		},
	}
	// aliases removal is skipped when refresh-app-awareness-ux is enabled
	if !refreshAppAwarenessUX {
		expected = append(expected, fakeOp{
			op:   "remove-snap-aliases",
			name: "some-snap",
		})
	}
	expected = append(expected, fakeOps{
		{
			op:          "run-inhibit-snap-for-unlink",
			name:        "some-snap",
			inhibitHint: "refresh",
		},
		{
			op:   "discard-namespace-locked",
			name: "some-snap",
		},
		{
			op:                 "unlink-snap",
			path:               filepath.Join(dirs.SnapMountDir, "some-snap/7"),
			unlinkSkipBinaries: refreshAppAwarenessUX,
			inhibitHint:        "refresh",
		},
		{
			op:   "copy-data",
			path: filepath.Join(dirs.SnapMountDir, "some-snap/11"),
			old:  filepath.Join(dirs.SnapMountDir, "some-snap/7"),
		},
		{
			op:   "setup-snap-save-data",
			path: filepath.Join(dirs.SnapDataSaveDir, "some-snap"),
		},
		{
			op:    "setup-profiles:Doing",
			name:  "some-snap",
			revno: snap.R(11),
		},
		{
			op: "candidate",
			sinfo: snap.SideInfo{
				RealName: "some-snap",
				SnapID:   "some-snap-id",
				Channel:  "some-channel",
				Revision: snap.R(11),
			},
		},
		{
			op:   "link-snap.failed",
			path: filepath.Join(dirs.SnapMountDir, "some-snap/11"),
		},
		{
			op:                 "unlink-snap",
			path:               filepath.Join(dirs.SnapMountDir, "some-snap/11"),
			unlinkSkipBinaries: false,
		},
		{
			op:    "setup-profiles:Undoing",
			name:  "some-snap",
			revno: snap.R(11),
		},
		{
			op:   "undo-copy-snap-data",
			path: filepath.Join(dirs.SnapMountDir, "some-snap/11"),
			old:  filepath.Join(dirs.SnapMountDir, "some-snap/7"),
		},
		{
			op:   "undo-setup-snap-save-data",
			path: filepath.Join(dirs.SnapDataSaveDir, "some-snap"),
			old:  filepath.Join(dirs.SnapDataSaveDir, "some-snap"),
		},
		{
			op:   "link-snap",
			path: filepath.Join(dirs.SnapMountDir, "some-snap/7"),
		},
		{
			op:     "maybe-set-next-boot",
			isUndo: true,
		},
	}...)
	// aliases removal undo is skipped when refresh-app-awareness-ux is enabled
	if !refreshAppAwarenessUX {
		expected = append(expected, fakeOp{
			op: "update-aliases",
		})
	}
	expected = append(expected, fakeOps{
		{
			op:    "undo-setup-snap",
			name:  "some-snap",
			path:  filepath.Join(dirs.SnapMountDir, "some-snap/11"),
			stype: "app",
		},
		{
			op:   "remove-snap-dir",
			name: "some-snap",
			path: filepath.Join(dirs.SnapMountDir, "some-snap"),
		},
		{
			op:   "storesvc-cleanup-download-artifacts",
			name: "",
			path: filepath.Join(dirs.SnapBlobDir, "some-snap_11.snap"),
			sha3: "<some-hash>",
		},
	}...)

	// ensure all our tasks ran
	c.Check(s.fakeStore.downloads, DeepEquals, []fakeDownload{{
		macaroon: s.user.StoreMacaroon,
		name:     "some-snap",
		target:   filepath.Join(dirs.SnapBlobDir, "some-snap_11.snap"),
	}})
	// start with an easier-to-read error if this fails:
	c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
	c.Assert(s.fakeBackend.ops, DeepEquals, expected)

	// verify snaps in the system state
	var snapst snapstate.SnapState
	err = snapstate.Get(s.state, "some-snap", &snapst)
	c.Assert(err, IsNil)

	c.Assert(snapst.Active, Equals, true)
	c.Assert(snapst.Sequence.Revisions, HasLen, 1)
	c.Assert(snapst.Sequence.Revisions[0], DeepEquals, sequence.NewRevisionSideState(&snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Channel:  "",
		Revision: snap.R(7),
	}, nil))
}

func (s *snapmgrTestSuite) TestUpdateUndoRunThrough(c *C) {
	s.testUpdateUndoRunThrough(c, false)
}

func (s *snapmgrTestSuite) TestUpdateUndoRunThroughSkipBinaries(c *C) {
	s.enableRefreshAppAwarenessUX()
	s.testUpdateUndoRunThrough(c, true)
}

func lastWithLane(tasks []*state.Task) *state.Task {
	for i := len(tasks) - 1; i >= 0; i-- {
		if lanes := tasks[i].Lanes(); len(lanes) == 1 && lanes[0] != 0 {
			return tasks[i]
		}
	}
	return nil
}

func (s *snapmgrTestSuite) TestUpdateUndoRestoresRevisionConfig(c *C) {
	var errorTaskExecuted bool

	// overwrite error-trigger task handler with custom one for this test
	erroringHandler := func(task *state.Task, _ *tomb.Tomb) error {
		st := task.State()
		st.Lock()
		defer st.Unlock()

		// modify current config of some-snap
		tr := config.NewTransaction(st)
		tr.Set("some-snap", "foo", "canary")
		tr.Commit()

		errorTaskExecuted = true
		return errors.New("error out")
	}
	s.o.TaskRunner().AddHandler("error-trigger", erroringHandler, nil)

	si := snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Revision: snap.R(7),
	}
	si2 := snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Revision: snap.R(6),
	}

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

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:          true,
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si2, &si}),
		TrackingChannel: "latest/stable",
		Current:         si.Revision,
		SnapType:        "app",
	})

	// set some configuration
	tr := config.NewTransaction(s.state)
	tr.Set("some-snap", "foo", "revision 7 value")
	tr.Commit()
	config.SaveRevisionConfig(s.state, "some-snap", snap.R(7))

	chg := s.state.NewChange("install", "install a snap")
	ts, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "some-channel"}, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)
	chg.AddAll(ts)

	last := lastWithLane(ts.Tasks())
	c.Assert(last, NotNil)

	terr := s.state.NewTask("error-trigger", "provoking total undo")
	terr.WaitFor(last)
	terr.JoinLane(last.Lanes()[0])
	chg.AddTask(terr)

	s.settle(c)

	c.Check(chg.Status(), Equals, state.ErrorStatus)
	c.Check(errorTaskExecuted, Equals, true)

	// after undoing the update some-snap config should be restored to that of rev.7
	var val string
	tr = config.NewTransaction(s.state)
	c.Assert(tr.Get("some-snap", "foo", &val), IsNil)
	c.Check(val, Equals, "revision 7 value")
}

func (s *snapmgrTestSuite) TestUpdateMakesConfigSnapshot(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)},
		}),
		Current:  snap.R(1),
		SnapType: "app",
	})

	tr := config.NewTransaction(s.state)
	tr.Set("some-snap", "foo", "bar")
	tr.Commit()

	var cfgs map[string]any
	// we don't have config snapshots yet
	c.Assert(s.state.Get("revision-config", &cfgs), testutil.ErrorIs, state.ErrNoState)

	chg := s.state.NewChange("update", "update a snap")
	opts := &snapstate.RevisionOptions{Channel: "some-channel", Revision: snap.R(2)}
	ts, err := snapstate.Update(s.state, "some-snap", opts, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)
	chg.AddAll(ts)

	restore := snapstate.MockEnsuredMountsUpdated(s.snapmgr, true)
	defer restore()

	s.settle(c)

	cfgs = nil
	// config copy of rev. 1 has been made
	c.Assert(s.state.Get("revision-config", &cfgs), IsNil)
	c.Assert(cfgs["some-snap"], DeepEquals, map[string]any{
		"1": map[string]any{
			"foo": "bar",
		},
	})
}

func (s *snapmgrTestSuite) testUpdateTotalUndoRunThrough(c *C, refreshAppAwarenessUX bool) {
	si := snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Revision: snap.R(7),
	}

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

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:          true,
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si}),
		TrackingChannel: "latest/stable",
		Current:         si.Revision,
		SnapType:        "app",
	})

	chg := s.state.NewChange("install", "install a snap")
	ts, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "some-channel"}, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)
	chg.AddAll(ts)

	// We need to make it not be rerefresh, and we could do just
	// that but instead we do the 'right' thing and attach it to
	// the last task that's on a lane.
	last := lastWithLane(ts.Tasks())
	c.Assert(last, NotNil)

	terr := s.state.NewTask("error-trigger", "provoking total undo")
	terr.WaitFor(last)
	terr.JoinLane(last.Lanes()[0])
	chg.AddTask(terr)

	s.settle(c)

	expected := fakeOps{
		{
			op: "storesvc-snap-action",
			curSnaps: []store.CurrentSnap{{
				InstanceName:    "some-snap",
				SnapID:          "some-snap-id",
				Revision:        snap.R(7),
				TrackingChannel: "latest/stable",
				RefreshedDate:   fakeRevDateEpoch.AddDate(0, 0, 7),
				Epoch:           snap.E("1*"),
				Resources:       make(map[string]snap.Revision),
			}},
			userID: 1,
		},
		{
			op: "storesvc-snap-action:action",
			action: store.SnapAction{
				Action:       "refresh",
				InstanceName: "some-snap",
				SnapID:       "some-snap-id",
				Channel:      "some-channel",
				Flags:        store.SnapActionEnforceValidation,
			},
			revno:  snap.R(11),
			userID: 1,
		},
		{
			op:   "storesvc-download",
			name: "some-snap",
		},
		{
			op:    "validate-snap:Doing",
			name:  "some-snap",
			revno: snap.R(11),
		},
		{
			op:  "current",
			old: filepath.Join(dirs.SnapMountDir, "some-snap/7"),
		},
		{
			op:   "open-snap-file",
			path: filepath.Join(dirs.SnapBlobDir, "some-snap_11.snap"),
			sinfo: snap.SideInfo{
				RealName: "some-snap",
				SnapID:   "some-snap-id",
				Channel:  "some-channel",
				Revision: snap.R(11),
			},
		},
		{
			op:    "setup-snap",
			name:  "some-snap",
			path:  filepath.Join(dirs.SnapBlobDir, "some-snap_11.snap"),
			revno: snap.R(11),
		},
	}
	// aliases removal is skipped when refresh-app-awareness-ux is enabled
	if !refreshAppAwarenessUX {
		expected = append(expected, fakeOp{
			op:   "remove-snap-aliases",
			name: "some-snap",
		})
	}
	expected = append(expected, fakeOps{
		{
			op:          "run-inhibit-snap-for-unlink",
			name:        "some-snap",
			inhibitHint: "refresh",
		},
		{
			op:   "discard-namespace-locked",
			name: "some-snap",
		},
		{
			op:                 "unlink-snap",
			path:               filepath.Join(dirs.SnapMountDir, "some-snap/7"),
			unlinkSkipBinaries: refreshAppAwarenessUX,
			inhibitHint:        "refresh",
		},
		{
			op:   "copy-data",
			path: filepath.Join(dirs.SnapMountDir, "some-snap/11"),
			old:  filepath.Join(dirs.SnapMountDir, "some-snap/7"),
		},
		{
			op:   "setup-snap-save-data",
			path: filepath.Join(dirs.SnapDataSaveDir, "some-snap"),
		},
		{
			op:    "setup-profiles:Doing",
			name:  "some-snap",
			revno: snap.R(11),
		},
		{
			op: "candidate",
			sinfo: snap.SideInfo{
				RealName: "some-snap",
				SnapID:   "some-snap-id",
				Channel:  "some-channel",
				Revision: snap.R(11),
			},
		},
		{
			op:   "link-snap",
			path: filepath.Join(dirs.SnapMountDir, "some-snap/11"),
		},
		{
			op: "maybe-set-next-boot",
		},
		{
			op:    "auto-connect:Doing",
			name:  "some-snap",
			revno: snap.R(11),
		},
		{
			op: "update-aliases",
		},
	}...)
	// undoing everything from here down...
	if refreshAppAwarenessUX {
		// refresh-app-awareness-ux changes setup-aliases undo behavior
		expected = append(expected, fakeOp{
			op: "update-aliases",
		})
	} else {
		expected = append(expected, fakeOp{
			op:   "remove-snap-aliases",
			name: "some-snap",
		})
	}
	expected = append(expected, fakeOps{
		{
			op:    "auto-connect:Undoing",
			name:  "some-snap",
			revno: snap.R(11),
		},
		{
			op:   "unlink-snap",
			path: filepath.Join(dirs.SnapMountDir, "some-snap/11"),
		},
		{
			op:    "setup-profiles:Undoing",
			name:  "some-snap",
			revno: snap.R(11),
		},
		{
			op:   "undo-copy-snap-data",
			path: filepath.Join(dirs.SnapMountDir, "some-snap/11"),
			old:  filepath.Join(dirs.SnapMountDir, "some-snap/7"),
		},
		{
			op:   "undo-setup-snap-save-data",
			path: filepath.Join(dirs.SnapDataSaveDir, "some-snap"),
			old:  filepath.Join(dirs.SnapDataSaveDir, "some-snap"),
		},
		{
			op:   "link-snap",
			path: filepath.Join(dirs.SnapMountDir, "some-snap/7"),
		},
		{
			op:     "maybe-set-next-boot",
			isUndo: true,
		},
	}...)
	// aliases removal undo is skipped when refresh-app-awareness-ux is enabled
	if !refreshAppAwarenessUX {
		expected = append(expected, fakeOp{
			op: "update-aliases",
		})
	}
	expected = append(expected, fakeOps{
		{
			op:    "undo-setup-snap",
			name:  "some-snap",
			path:  filepath.Join(dirs.SnapMountDir, "some-snap/11"),
			stype: "app",
		},
		{
			op:   "remove-snap-dir",
			name: "some-snap",
			path: filepath.Join(dirs.SnapMountDir, "some-snap"),
		},
		{
			op:   "storesvc-cleanup-download-artifacts",
			sha3: "<some-hash>",
			path: filepath.Join(dirs.SnapBlobDir, "some-snap_11.snap"),
		},
	}...)

	// ensure all our tasks ran
	c.Check(s.fakeStore.downloads, DeepEquals, []fakeDownload{{
		macaroon: s.user.StoreMacaroon,
		name:     "some-snap",
		target:   filepath.Join(dirs.SnapBlobDir, "some-snap_11.snap"),
	}})
	// friendlier failure first
	c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
	c.Assert(s.fakeBackend.ops, DeepEquals, expected)

	// verify snaps in the system state
	var snapst snapstate.SnapState
	err = snapstate.Get(s.state, "some-snap", &snapst)
	c.Assert(err, IsNil)

	c.Assert(snapst.Active, Equals, true)
	c.Assert(snapst.TrackingChannel, Equals, "latest/stable")
	c.Assert(snapst.Sequence.Revisions, HasLen, 1)
	c.Assert(snapst.Sequence.Revisions[0], DeepEquals, sequence.NewRevisionSideState(&snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Channel:  "",
		Revision: snap.R(7),
	}, nil))
}

func (s *snapmgrTestSuite) TestUpdateTotalUndoRunThrough(c *C) {
	s.testUpdateTotalUndoRunThrough(c, false)
}

func (s *snapmgrTestSuite) TestUpdateTotalUndoRunThroughSkipBinaries(c *C) {
	s.enableRefreshAppAwarenessUX()
	s.testUpdateTotalUndoRunThrough(c, true)
}

func (s *snapmgrTestSuite) TestUpdateSameRevision(c *C) {
	si := snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Revision: snap.R(7),
	}

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

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:          true,
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si}),
		TrackingChannel: "channel-for-7/stable",
		Current:         si.Revision,
	})

	_, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "channel-for-7/stable"}, s.user.ID, snapstate.Flags{})
	c.Assert(err.Error(), Equals, "snap has no updates available")
}

func (s *snapmgrTestSuite) TestUpdateToRevisionRememberedUserRunThrough(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-snap", Revision: snap.R(5), SnapID: "some-snap-id"},
		}),
		Current:  snap.R(5),
		SnapType: "app",
		UserID:   1,
	})

	chg := s.state.NewChange("refresh", "refresh a snap")
	ts, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "some-channel", Revision: snap.R(11)}, 0, snapstate.Flags{})
	c.Assert(err, IsNil)
	chg.AddAll(ts)

	s.settle(c)

	c.Assert(chg.Status(), Equals, state.DoneStatus)
	c.Assert(chg.Err(), IsNil)

	for _, op := range s.fakeBackend.ops {
		switch op.op {
		case "storesvc-snap-action:action":
			c.Check(op.userID, Equals, 1)
		case "storesvc-download":
			snapName := op.name
			c.Check(s.fakeStore.downloads[0], DeepEquals, fakeDownload{
				macaroon: "macaroon",
				name:     "some-snap",
				target:   filepath.Join(dirs.SnapBlobDir, "some-snap_11.snap"),
			}, Commentf(snapName))
		}
	}
}

// A noResultsStore returns no results for install/refresh requests
type noResultsStore struct {
	*fakeStore
}

func (n noResultsStore) SnapAction(ctx context.Context, currentSnaps []*store.CurrentSnap, actions []*store.SnapAction, assertQuery store.AssertionQuery, user *auth.UserState, opts *store.RefreshOptions) ([]store.SnapActionResult, []store.AssertionResult, error) {
	if assertQuery != nil {
		panic("no assertion query support")
	}
	return nil, nil, &store.SnapActionError{NoResults: true}
}

func (s *snapmgrTestSuite) TestUpdateNoStoreResults(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	snapstate.ReplaceStore(s.state, noResultsStore{fakeStore: s.fakeStore})

	// this is an atypical case in which the store didn't return
	// an error nor a result, we are defensive and return
	// a reasonable error
	si := snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Revision: snap.R(7),
	}

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:          true,
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si}),
		TrackingChannel: "channel-for-7/stable",
		Current:         si.Revision,
	})

	_, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "channel-for-7"}, s.user.ID, snapstate.Flags{})
	c.Assert(err, Equals, snapstate.ErrMissingExpectedResult)
}

func (s *snapmgrTestSuite) TestUpdateNoStoreResultsWithChannelChange(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	snapstate.ReplaceStore(s.state, noResultsStore{fakeStore: s.fakeStore})

	// this is an atypical case in which the store didn't return
	// an error nor a result, we are defensive and return
	// a reasonable error
	si := snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Revision: snap.R(7),
	}

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:          true,
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si}),
		TrackingChannel: "channel-for-9/stable",
		Current:         si.Revision,
	})

	_, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "channel-for-7"}, s.user.ID, snapstate.Flags{})
	c.Assert(err, Equals, snapstate.ErrMissingExpectedResult)
}

func (s *snapmgrTestSuite) TestUpdateSameRevisionSwitchesChannel(c *C) {
	si := snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Revision: snap.R(7),
	}

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

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:          true,
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si}),
		TrackingChannel: "other-channel/stable",
		Current:         si.Revision,
	})

	ts, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "channel-for-7/stable"}, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)
	c.Check(ts.Tasks(), HasLen, 1)
	c.Check(ts.Tasks()[0].Kind(), Equals, "switch-snap-channel")
	c.Check(ts.MaybeEdge(snapstate.SnapSetupEdge).Kind(), Equals, "switch-snap-channel")
}

func (s *snapmgrTestSuite) TestUpdateSameRevisionSwitchesChannelConflict(c *C) {
	si := snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Revision: snap.R(7),
	}

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

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:          true,
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si}),
		TrackingChannel: "other-channel/stable",
		Current:         si.Revision,
	})

	ts, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "channel-for-7"}, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)
	// make it visible
	s.state.NewChange("refresh", "refresh a snap").AddAll(ts)

	_, err = snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "channel-for-7"}, s.user.ID, snapstate.Flags{})
	c.Check(err, ErrorMatches, `snap "some-snap" has "refresh" change in progress`)
}

func (s *snapmgrTestSuite) TestUpdateSameRevisionSwitchChannelRunThrough(c *C) {
	si := snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Channel:  "other-channel",
		Revision: snap.R(7),
	}

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

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:          true,
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si}),
		TrackingChannel: "other-channel/stable",
		Current:         si.Revision,
	})

	ts, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "channel-for-7/stable"}, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)
	chg := s.state.NewChange("refresh", "refresh a snap")
	chg.AddAll(ts)

	// no local modifications, hence no edge
	te := ts.MaybeEdge(snapstate.LastBeforeLocalModificationsEdge)
	c.Assert(te, IsNil)

	s.settle(c)

	expected := fakeOps{
		// we just expect the "storesvc-snap-action" ops, we
		// don't have a fakeOp for switchChannel because it has
		// not a backend method, it just manipulates the state
		{
			op: "storesvc-snap-action",
			curSnaps: []store.CurrentSnap{{
				InstanceName:    "some-snap",
				SnapID:          "some-snap-id",
				Revision:        snap.R(7),
				TrackingChannel: "other-channel/stable",
				RefreshedDate:   fakeRevDateEpoch.AddDate(0, 0, 7),
				Epoch:           snap.E("1*"),
				Resources:       make(map[string]snap.Revision),
			}},
			userID: 1,
		},

		{
			op: "storesvc-snap-action:action",
			action: store.SnapAction{
				Action:       "refresh",
				InstanceName: "some-snap",
				SnapID:       "some-snap-id",
				Channel:      "channel-for-7/stable",
				Flags:        store.SnapActionEnforceValidation,
			},
			userID: 1,
		},
	}

	// start with an easier-to-read error if this fails:
	c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
	c.Assert(s.fakeBackend.ops, DeepEquals, expected)

	// verify snapSetup info
	var snapsup snapstate.SnapSetup
	task := ts.Tasks()[0]
	err = task.Get("snap-setup", &snapsup)
	c.Assert(err, IsNil)
	c.Assert(snapsup, DeepEquals, snapstate.SnapSetup{
		Channel:   "channel-for-7/stable",
		UserID:    s.user.ID,
		Type:      "app",
		PlugsOnly: true,
		Version:   "some-snapVer",
		SideInfo:  snapsup.SideInfo,
		Flags: snapstate.Flags{
			Transaction: client.TransactionPerSnap,
		},
		SnapPath: filepath.Join(dirs.SnapBlobDir, "some-snap_7.snap"),
	})
	c.Assert(snapsup.SideInfo, DeepEquals, &snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Revision: snap.R(7),
		Channel:  "channel-for-7/stable",
	})

	// verify snaps in the system state
	var snapst snapstate.SnapState
	err = snapstate.Get(s.state, "some-snap", &snapst)
	c.Assert(err, IsNil)

	c.Assert(snapst.Active, Equals, true)
	c.Assert(snapst.Sequence.Revisions, HasLen, 1)
	c.Assert(snapst.Sequence.Revisions[0], DeepEquals, sequence.NewRevisionSideState(&snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Channel:  "channel-for-7/stable",
		Revision: snap.R(7),
	}, nil))
}

func (s *snapmgrTestSuite) TestUpdateSameRevisionToggleIgnoreValidation(c *C) {
	si := snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Revision: snap.R(7),
	}

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

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:          true,
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si}),
		TrackingChannel: "channel-for-7/stable",
		Current:         si.Revision,
	})

	ts, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "channel-for-7/stable"}, s.user.ID, snapstate.Flags{IgnoreValidation: true})
	c.Assert(err, IsNil)
	c.Check(ts.Tasks(), HasLen, 1)
	c.Check(ts.Tasks()[0].Kind(), Equals, "toggle-snap-flags")
}

func (s *snapmgrTestSuite) TestUpdateSameRevisionToggleIgnoreValidationConflict(c *C) {
	si := snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Revision: snap.R(7),
	}

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

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:          true,
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si}),
		TrackingChannel: "channel-for-7/stable",
		Current:         si.Revision,
	})

	ts, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "channel-for-7"}, s.user.ID, snapstate.Flags{IgnoreValidation: true})
	c.Assert(err, IsNil)
	// make it visible
	s.state.NewChange("refresh", "refresh a snap").AddAll(ts)

	_, err = snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "channel-for-7"}, s.user.ID, snapstate.Flags{IgnoreValidation: true})
	c.Check(err, ErrorMatches, `snap "some-snap" has "refresh" change in progress`)

}

func (s *snapmgrTestSuite) TestUpdateSameRevisionToggleIgnoreValidationRunThrough(c *C) {
	si := snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Revision: snap.R(7),
		Channel:  "channel-for-7",
	}

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

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:          true,
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si}),
		TrackingChannel: "channel-for-7/stable",
		Current:         si.Revision,
	})

	ts, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "channel-for-7/stable"}, s.user.ID, snapstate.Flags{IgnoreValidation: true})
	c.Assert(err, IsNil)

	chg := s.state.NewChange("refresh", "refresh a snap")
	chg.AddAll(ts)

	s.settle(c)

	// verify snapSetup info
	var snapsup snapstate.SnapSetup
	task := ts.Tasks()[0]
	err = task.Get("snap-setup", &snapsup)
	c.Assert(err, IsNil)
	c.Check(snapsup, DeepEquals, snapstate.SnapSetup{
		SideInfo:  snapsup.SideInfo,
		Channel:   "channel-for-7/stable",
		UserID:    s.user.ID,
		Type:      "app",
		PlugsOnly: true,
		Version:   "some-snapVer",
		Flags: snapstate.Flags{
			IgnoreValidation: true,
			Transaction:      client.TransactionPerSnap,
		},
		SnapPath: filepath.Join(dirs.SnapBlobDir, "some-snap_7.snap"),
	})
	c.Check(snapsup.SideInfo, DeepEquals, &snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Revision: snap.R(7),
		Channel:  "channel-for-7/stable",
	})

	// verify snaps in the system state
	var snapst snapstate.SnapState
	err = snapstate.Get(s.state, "some-snap", &snapst)
	c.Assert(err, IsNil)

	c.Check(snapst.Active, Equals, true)
	c.Check(snapst.Sequence.Revisions, HasLen, 1)
	c.Check(snapst.Sequence.Revisions[0], DeepEquals, sequence.NewRevisionSideState(&snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Channel:  "channel-for-7",
		Revision: snap.R(7),
	}, nil))
	c.Check(snapst.IgnoreValidation, Equals, true)
}

func (s *snapmgrTestSuite) TestUpdateValidateRefreshesSaysNo(c *C) {
	si := snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Revision: snap.R(7),
	}

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

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si}),
		Current:  si.Revision,
	})

	validateErr := errors.New("refresh control error")
	validateRefreshes := func(st *state.State, refreshes []*snap.Info, ignoreValidation map[string]bool, userID int, deviceCtx snapstate.DeviceContext) ([]*snap.Info, error) {
		c.Check(refreshes, HasLen, 1)
		c.Check(refreshes[0].SnapID, Equals, "some-snap-id")
		c.Check(refreshes[0].Revision, Equals, snap.R(11))
		c.Check(ignoreValidation, HasLen, 0)
		return nil, validateErr
	}
	// hook it up
	snapstate.ValidateRefreshes = validateRefreshes

	_, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "stable"}, s.user.ID, snapstate.Flags{})
	c.Assert(err, Equals, validateErr)
}

func (s *snapmgrTestSuite) TestUpdateValidateRefreshesSaysNoButIgnoreValidationIsSet(c *C) {
	si := snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Revision: snap.R(7),
	}

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

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si}),
		Current:  si.Revision,
		SnapType: "app",
	})

	validateErr := errors.New("refresh control error")
	validateRefreshes := func(st *state.State, refreshes []*snap.Info, ignoreValidation map[string]bool, userID int, deviceCtx snapstate.DeviceContext) ([]*snap.Info, error) {
		return nil, validateErr
	}
	// hook it up
	snapstate.ValidateRefreshes = validateRefreshes

	flags := snapstate.Flags{JailMode: true, IgnoreValidation: true, Transaction: client.TransactionPerSnap}
	ts, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "stable"}, s.user.ID, flags)
	c.Assert(err, IsNil)

	var snapsup snapstate.SnapSetup
	err = ts.Tasks()[0].Get("snap-setup", &snapsup)
	c.Assert(err, IsNil)
	c.Check(snapsup.Flags, DeepEquals, flags.ForSnapSetup())
}

func (s *snapmgrTestSuite) TestUpdateIgnoreValidationSticky(c *C) {
	si := snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Revision: snap.R(7),
	}

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

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si}),
		Current:  si.Revision,
		SnapType: "app",
	})

	validateErr := errors.New("refresh control error")
	validateRefreshesFail := func(st *state.State, refreshes []*snap.Info, ignoreValidation map[string]bool, userID int, deviceCtx snapstate.DeviceContext) ([]*snap.Info, error) {
		c.Check(refreshes, HasLen, 1)
		if len(ignoreValidation) == 0 {
			return nil, validateErr
		}
		c.Check(ignoreValidation, DeepEquals, map[string]bool{
			"some-snap": true,
		})
		return refreshes, nil
	}
	// hook it up
	snapstate.ValidateRefreshes = validateRefreshesFail

	flags := snapstate.Flags{IgnoreValidation: true}
	ts, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "stable"}, s.user.ID, flags)
	c.Assert(err, IsNil)

	c.Check(s.fakeBackend.ops[0], DeepEquals, fakeOp{
		op: "storesvc-snap-action",
		curSnaps: []store.CurrentSnap{{
			InstanceName:     "some-snap",
			SnapID:           "some-snap-id",
			Revision:         snap.R(7),
			IgnoreValidation: false,
			RefreshedDate:    fakeRevDateEpoch.AddDate(0, 0, 7),
			Epoch:            snap.E("1*"),
			Resources:        make(map[string]snap.Revision),
		}},
		userID: 1,
	})
	c.Check(s.fakeBackend.ops[1], DeepEquals, fakeOp{
		op:    "storesvc-snap-action:action",
		revno: snap.R(11),
		action: store.SnapAction{
			Action:       "refresh",
			InstanceName: "some-snap",
			SnapID:       "some-snap-id",
			Channel:      "stable",
			Flags:        store.SnapActionIgnoreValidation,
		},
		userID: 1,
	})

	chg := s.state.NewChange("refresh", "refresh snap")
	chg.AddAll(ts)

	s.settle(c)

	// verify snap has IgnoreValidation set
	var snapst snapstate.SnapState
	err = snapstate.Get(s.state, "some-snap", &snapst)
	c.Assert(err, IsNil)
	c.Check(snapst.IgnoreValidation, Equals, true)
	c.Check(snapst.Current, Equals, snap.R(11))

	s.fakeBackend.ops = nil
	s.fakeStore.refreshRevnos = map[string]snap.Revision{
		"some-snap-id": snap.R(12),
	}
	_, tts, err := snapstate.UpdateMany(context.Background(), s.state, []string{"some-snap"}, nil, s.user.ID, nil)
	c.Assert(err, IsNil)
	c.Check(tts, HasLen, 3)
	verifyReRefreshTasks(c, tts[1])

	c.Check(s.fakeBackend.ops[0], DeepEquals, fakeOp{
		op: "storesvc-snap-action",
		curSnaps: []store.CurrentSnap{{
			InstanceName:     "some-snap",
			SnapID:           "some-snap-id",
			Revision:         snap.R(11),
			TrackingChannel:  "latest/stable",
			IgnoreValidation: true,
			RefreshedDate:    fakeRevDateEpoch.AddDate(0, 0, 11),
			Epoch:            snap.E("1*"),
			Resources:        make(map[string]snap.Revision),
		}},
		userID: 1,
	})
	c.Check(s.fakeBackend.ops[1], DeepEquals, fakeOp{
		op:    "storesvc-snap-action:action",
		revno: snap.R(12),
		action: store.SnapAction{
			Action:       "refresh",
			InstanceName: "some-snap",
			SnapID:       "some-snap-id",
			Channel:      "latest/stable",
			Flags:        store.SnapActionIgnoreValidation,
		},
		userID: 1,
	})

	chg = s.state.NewChange("refresh", "refresh snaps")
	chg.AddAll(tts[0])

	s.settle(c)

	snapst = snapstate.SnapState{}
	err = snapstate.Get(s.state, "some-snap", &snapst)
	c.Assert(err, IsNil)
	c.Check(snapst.IgnoreValidation, Equals, true)
	c.Check(snapst.Current, Equals, snap.R(12))

	// reset ignore validation
	s.fakeBackend.ops = nil
	s.fakeStore.refreshRevnos = map[string]snap.Revision{
		"some-snap-id": snap.R(11),
	}
	validateRefreshes := func(st *state.State, refreshes []*snap.Info, ignoreValidation map[string]bool, userID int, deviceCtx snapstate.DeviceContext) ([]*snap.Info, error) {
		return refreshes, nil
	}
	// hook it up
	snapstate.ValidateRefreshes = validateRefreshes
	flags = snapstate.Flags{}
	ts, err = snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "stable"}, s.user.ID, flags)
	c.Assert(err, IsNil)

	c.Check(s.fakeBackend.ops[0], DeepEquals, fakeOp{
		op: "storesvc-snap-action",
		curSnaps: []store.CurrentSnap{{
			InstanceName:     "some-snap",
			SnapID:           "some-snap-id",
			Revision:         snap.R(12),
			TrackingChannel:  "latest/stable",
			IgnoreValidation: true,
			RefreshedDate:    fakeRevDateEpoch.AddDate(0, 0, 12),
			Epoch:            snap.E("1*"),
			Resources:        make(map[string]snap.Revision),
		}},
		userID: 1,
	})
	c.Check(s.fakeBackend.ops[1], DeepEquals, fakeOp{
		op:    "storesvc-snap-action:action",
		revno: snap.R(11),
		action: store.SnapAction{
			Action:       "refresh",
			InstanceName: "some-snap",
			SnapID:       "some-snap-id",
			Channel:      "latest/stable",
			Flags:        store.SnapActionEnforceValidation,
		},
		userID: 1,
	})

	chg = s.state.NewChange("refresh", "refresh snap")
	chg.AddAll(ts)

	s.settle(c)

	snapst = snapstate.SnapState{}
	err = snapstate.Get(s.state, "some-snap", &snapst)
	c.Assert(err, IsNil)
	c.Check(snapst.IgnoreValidation, Equals, false)
	c.Check(snapst.Current, Equals, snap.R(11))
}

func (s *snapmgrTestSuite) TestParallelInstanceUpdateIgnoreValidationSticky(c *C) {
	si := snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Revision: snap.R(7),
	}

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

	tr := config.NewTransaction(s.state)
	tr.Set("core", "experimental.parallel-instances", true)
	tr.Commit()

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si}),
		Current:  si.Revision,
		SnapType: "app",
	})
	snapstate.Set(s.state, "some-snap_instance", &snapstate.SnapState{
		Active:      true,
		Sequence:    snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si}),
		Current:     si.Revision,
		SnapType:    "app",
		InstanceKey: "instance",
	})

	validateErr := errors.New("refresh control error")
	validateRefreshesFail := func(st *state.State, refreshes []*snap.Info, ignoreValidation map[string]bool, userID int, deviceCtx snapstate.DeviceContext) ([]*snap.Info, error) {
		c.Check(refreshes, HasLen, 2)
		if len(ignoreValidation) == 0 {
			return nil, validateErr
		}
		c.Check(ignoreValidation, DeepEquals, map[string]bool{
			"some-snap_instance": true,
		})
		return refreshes, nil
	}
	// hook it up
	snapstate.ValidateRefreshes = validateRefreshesFail

	flags := snapstate.Flags{IgnoreValidation: true}
	ts, err := snapstate.Update(s.state, "some-snap_instance", &snapstate.RevisionOptions{Channel: "stable"}, s.user.ID, flags)
	c.Assert(err, IsNil)

	c.Check(s.fakeBackend.ops[0], DeepEquals, fakeOp{
		op: "storesvc-snap-action",
		curSnaps: []store.CurrentSnap{{
			InstanceName:     "some-snap",
			SnapID:           "some-snap-id",
			Revision:         snap.R(7),
			IgnoreValidation: false,
			RefreshedDate:    fakeRevDateEpoch.AddDate(0, 0, 7),
			Epoch:            snap.E("1*"),
			Resources:        make(map[string]snap.Revision),
		}, {
			InstanceName:     "some-snap_instance",
			SnapID:           "some-snap-id",
			Revision:         snap.R(7),
			IgnoreValidation: false,
			RefreshedDate:    fakeRevDateEpoch.AddDate(0, 0, 7),
			Epoch:            snap.E("1*"),
			Resources:        make(map[string]snap.Revision),
		}},
		userID: 1,
	})
	c.Check(s.fakeBackend.ops[1], DeepEquals, fakeOp{
		op:    "storesvc-snap-action:action",
		revno: snap.R(11),
		action: store.SnapAction{
			Action:       "refresh",
			InstanceName: "some-snap_instance",
			SnapID:       "some-snap-id",
			Channel:      "stable",
			Flags:        store.SnapActionIgnoreValidation,
		},
		userID: 1,
	})

	chg := s.state.NewChange("refresh", "refresh snaps")
	chg.AddAll(ts)

	s.settle(c)

	// ensure all our tasks ran
	c.Assert(chg.Err(), IsNil)
	c.Assert(chg.IsReady(), Equals, true)

	// verify snap 'instance' has IgnoreValidation set and the snap was
	// updated
	var snapst snapstate.SnapState
	err = snapstate.Get(s.state, "some-snap_instance", &snapst)
	c.Assert(err, IsNil)
	c.Check(snapst.IgnoreValidation, Equals, true)
	c.Check(snapst.Current, Equals, snap.R(11))
	// and the other snap does not
	err = snapstate.Get(s.state, "some-snap", &snapst)
	c.Assert(err, IsNil)
	c.Check(snapst.Current, Equals, snap.R(7))
	c.Check(snapst.IgnoreValidation, Equals, false)

	s.fakeBackend.ops = nil
	s.fakeStore.refreshRevnos = map[string]snap.Revision{
		"some-snap-id": snap.R(12),
	}
	updates, tts, err := snapstate.UpdateMany(context.Background(), s.state, []string{"some-snap", "some-snap_instance"}, nil, s.user.ID, nil)
	c.Assert(err, IsNil)
	c.Check(tts, HasLen, 4)
	verifyReRefreshTasks(c, tts[2])
	verifyDelayedEffectsTasks(c, tts[3], []int{2, 3}, 0)
	sort.Strings(updates)
	c.Check(updates, DeepEquals, []string{"some-snap", "some-snap_instance"})

	chg = s.state.NewChange("refresh", "refresh snaps")
	for _, ts := range tts[:len(tts)-1] {
		chg.AddAll(ts)
	}

	s.settle(c)

	// ensure all our tasks ran
	c.Assert(chg.Err(), IsNil)
	c.Assert(chg.IsReady(), Equals, true)

	err = snapstate.Get(s.state, "some-snap", &snapst)
	c.Assert(err, IsNil)
	c.Check(snapst.IgnoreValidation, Equals, false)
	c.Check(snapst.Current, Equals, snap.R(12))

	err = snapstate.Get(s.state, "some-snap_instance", &snapst)
	c.Assert(err, IsNil)
	c.Check(snapst.IgnoreValidation, Equals, true)
	c.Check(snapst.Current, Equals, snap.R(12))

	for i := 0; i < 2; i++ {
		op := s.fakeBackend.ops[i]
		switch op.op {
		case "storesvc-snap-action":
			c.Check(op, DeepEquals, fakeOp{
				op: "storesvc-snap-action",
				curSnaps: []store.CurrentSnap{{
					InstanceName:     "some-snap",
					SnapID:           "some-snap-id",
					Revision:         snap.R(7),
					IgnoreValidation: false,
					RefreshedDate:    fakeRevDateEpoch.AddDate(0, 0, 7),
					Epoch:            snap.E("1*"),
					Resources:        make(map[string]snap.Revision),
				}, {
					InstanceName:     "some-snap_instance",
					SnapID:           "some-snap-id",
					Revision:         snap.R(11),
					TrackingChannel:  "latest/stable",
					IgnoreValidation: true,
					RefreshedDate:    fakeRevDateEpoch.AddDate(0, 0, 11),
					Epoch:            snap.E("1*"),
					Resources:        make(map[string]snap.Revision),
				}},
				userID: 1,
			})
		case "storesvc-snap-action:action":
			switch op.action.InstanceName {
			case "some-snap":
				c.Check(op, DeepEquals, fakeOp{
					op:    "storesvc-snap-action:action",
					revno: snap.R(12),
					action: store.SnapAction{
						Action:       "refresh",
						InstanceName: "some-snap",
						SnapID:       "some-snap-id",
						Flags:        0,
					},
					userID: 1,
				})
			case "some-snap_instance":
				c.Check(op, DeepEquals, fakeOp{
					op:    "storesvc-snap-action:action",
					revno: snap.R(12),
					action: store.SnapAction{
						Action:       "refresh",
						InstanceName: "some-snap_instance",
						SnapID:       "some-snap-id",
						Channel:      "latest/stable",
						Flags:        store.SnapActionIgnoreValidation,
					},
					userID: 1,
				})
			default:
				c.Fatalf("unexpected instance name %q", op.action.InstanceName)
			}
		default:
			c.Fatalf("unexpected action %q", op.op)
		}
	}

}

func (s *snapmgrTestSuite) TestUpdateFromLocal(c *C) {
	si := snap.SideInfo{
		RealName: "some-snap",
		Revision: snap.R("x1"),
	}

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

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:          true,
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si}),
		TrackingChannel: "channel-for-7/stable",
		Current:         si.Revision,
	})

	_, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "channel-for-7"}, s.user.ID, snapstate.Flags{})
	c.Assert(err, Equals, store.ErrLocalSnap)
}

func (s *snapmgrTestSuite) TestUpdateAmend(c *C) {
	si := snap.SideInfo{
		RealName: "some-snap",
		Revision: snap.R("x1"),
	}

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

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:          true,
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si}),
		TrackingChannel: "channel-for-7/stable",
		Current:         si.Revision,
	})

	ts, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "channel-for-7"}, s.user.ID, snapstate.Flags{Amend: true})
	c.Assert(err, IsNil)
	verifyUpdateTasks(c, snap.TypeApp, doesReRefresh|mockDelayedEffects, 0, ts)

	// ensure we go from local to store revision-7
	var snapsup snapstate.SnapSetup
	tasks := ts.Tasks()
	c.Check(tasks[1].Kind(), Equals, "download-snap")
	err = tasks[1].Get("snap-setup", &snapsup)
	c.Assert(err, IsNil)
	c.Check(snapsup.Revision(), Equals, snap.R(7))
}

func (s *snapmgrTestSuite) TestUpdateAmendToLocalRevWithoutFlag(c *C) {
	si := snap.SideInfo{
		RealName: "some-snap",
		Revision: snap.R("x1"),
	}

	otherSI := snap.SideInfo{
		RealName: "some-snap",
		Revision: snap.R("x2"),
	}

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

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			&si,
			&otherSI,
		}),
		TrackingChannel: "channel-for-7/stable",
		Current:         si.Revision,
	})

	ts, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{
		Revision: otherSI.Revision,
	}, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)
	verifyUpdateTasks(c, snap.TypeApp, doesReRefresh|localRevision|mockDelayedEffects, 0, ts)

	// ensure we go from local to local revision x2
	var snapsup snapstate.SnapSetup
	tasks := ts.Tasks()
	c.Check(tasks[1].Kind(), Equals, "prepare-snap")
	err = tasks[1].Get("snap-setup", &snapsup)
	c.Assert(err, IsNil)
	c.Check(snapsup.Revision(), Equals, otherSI.Revision)
}

func (s *snapmgrTestSuite) TestUpdateAmendSnapNotFound(c *C) {
	si := snap.SideInfo{
		RealName: "snap-unknown",
		Revision: snap.R("x1"),
	}

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

	snapstate.Set(s.state, "snap-unknown", &snapstate.SnapState{
		Active:          true,
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si}),
		TrackingChannel: "latest/stable",
		Current:         si.Revision,
	})

	_, err := snapstate.Update(s.state, "snap-unknown", &snapstate.RevisionOptions{Channel: "stable"}, s.user.ID, snapstate.Flags{Amend: true})
	c.Assert(err, Equals, store.ErrSnapNotFound)
}

func (s *snapmgrTestSuite) TestSingleUpdateBlockedRevision(c *C) {
	// single updates should *not* set the block list
	si7 := snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Revision: snap.R(7),
	}
	si11 := snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Revision: snap.R(11),
	}

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

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si7, &si11}),
		Current:  si7.Revision,
		SnapType: "app",
	})

	_, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "some-channel"}, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)

	c.Assert(s.fakeBackend.ops, HasLen, 2)
	c.Check(s.fakeBackend.ops[0], DeepEquals, fakeOp{
		op: "storesvc-snap-action",
		curSnaps: []store.CurrentSnap{{
			InstanceName:  "some-snap",
			SnapID:        "some-snap-id",
			Revision:      snap.R(7),
			RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 7),
			Epoch:         snap.E("1*"),
			Resources:     make(map[string]snap.Revision),
		}},
		userID: 1,
	})
}

func (s *snapmgrTestSuite) TestMultiUpdateBlockedRevision(c *C) {
	// multi-updates should *not* set the block list
	si7 := snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Revision: snap.R(7),
	}
	si11 := snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Revision: snap.R(11),
	}

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

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si7, &si11}),
		Current:  si7.Revision,
		SnapType: "app",
	})

	updates, _, err := snapstate.UpdateMany(context.Background(), s.state, []string{"some-snap"}, nil, s.user.ID, nil)
	c.Assert(err, IsNil)
	c.Check(updates, DeepEquals, []string{"some-snap"})

	c.Assert(s.fakeBackend.ops, HasLen, 2)
	c.Check(s.fakeBackend.ops[0], DeepEquals, fakeOp{
		op: "storesvc-snap-action",
		curSnaps: []store.CurrentSnap{{
			InstanceName:  "some-snap",
			SnapID:        "some-snap-id",
			Revision:      snap.R(7),
			RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 7),
			Epoch:         snap.E("1*"),
			Resources:     make(map[string]snap.Revision),
		}},
		userID: 1,
	})
}

func (s *snapmgrTestSuite) TestAllUpdateBlockedRevision(c *C) {
	//  update-all *should* set the block list
	si7 := snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Revision: snap.R(7),
	}
	si11 := snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Revision: snap.R(11),
	}

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

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si7, &si11}),
		Current:  si7.Revision,
	})

	updates, _, err := snapstate.UpdateMany(context.Background(), s.state, nil, nil, s.user.ID, nil)
	c.Check(err, IsNil)
	c.Check(updates, HasLen, 0)

	c.Assert(s.fakeBackend.ops, HasLen, 2)
	c.Check(s.fakeBackend.ops[0], DeepEquals, fakeOp{
		op: "storesvc-snap-action",
		curSnaps: []store.CurrentSnap{{
			InstanceName:  "some-snap",
			SnapID:        "some-snap-id",
			Revision:      snap.R(7),
			RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 7),
			Block:         []snap.Revision{snap.R(11)},
			Epoch:         snap.E("1*"),
			Resources:     make(map[string]snap.Revision),
		}},
		userID: 1,
	})
}

func (s *snapmgrTestSuite) TestAllUpdateRevisionNotBlocked(c *C) {
	//  update-all *should* set the block list
	si7 := snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Revision: snap.R(7),
	}
	si11 := snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Revision: snap.R(11),
	}

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

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si7, &si11}),
		Current:  si7.Revision,
		RevertStatus: map[int]snapstate.RevertStatus{
			si7.Revision.N: snapstate.NotBlocked,
		},
	})

	updates, _, err := snapstate.UpdateMany(context.Background(), s.state, nil, nil, s.user.ID, nil)
	c.Check(err, IsNil)
	c.Check(updates, HasLen, 0)

	c.Assert(s.fakeBackend.ops, HasLen, 2)
	c.Check(s.fakeBackend.ops[0], DeepEquals, fakeOp{
		op: "storesvc-snap-action",
		curSnaps: []store.CurrentSnap{{
			InstanceName:  "some-snap",
			SnapID:        "some-snap-id",
			Revision:      snap.R(7),
			RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 7),
			Block:         []snap.Revision{snap.R(11)},
			Epoch:         snap.E("1*"),
			Resources:     make(map[string]snap.Revision),
		}},
		userID: 1,
	})
}

func (s *snapmgrTestSuite) TestUpdateManyPartialFailureCheckRerefreshDone(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	snapstate.CanAutoRefresh = func(*state.State) (bool, error) { return true, nil }
	makeTestRefreshConfig(s.state)

	var someSnapValidation bool

	// override validate-snap handler set by AddForeignTaskHandlers.
	s.o.TaskRunner().AddHandler("validate-snap", func(t *state.Task, _ *tomb.Tomb) error {
		t.State().Lock()
		defer t.State().Unlock()
		snapsup, err := snapstate.TaskSnapSetup(t)
		c.Assert(err, IsNil)
		if snapsup.SnapName() == "some-snap" {
			someSnapValidation = true
			return fmt.Errorf("boom")
		}
		return nil
	}, nil)

	snapstate.Set(s.state, "some-other-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-other-snap", SnapID: "some-other-snap-id", Revision: snap.R(1)},
		}),
		Current:  snap.R(1),
		SnapType: "app",
	})
	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)},
		}),
		Current:  snap.R(1),
		SnapType: "app",
	})

	validateRefreshes := func(st *state.State, refreshes []*snap.Info, ignoreValidation map[string]bool, userID int, deviceCtx snapstate.DeviceContext) ([]*snap.Info, error) {
		c.Check(refreshes, HasLen, 2)
		c.Check(ignoreValidation, HasLen, 0)
		return refreshes, nil
	}
	// hook it up
	snapstate.ValidateRefreshes = validateRefreshes

	s.state.Unlock()
	s.snapmgr.Ensure()
	s.state.Lock()

	c.Assert(s.state.Changes(), HasLen, 1)
	chg := s.state.Changes()[0]
	c.Check(chg.Kind(), Equals, "auto-refresh")
	c.Check(chg.IsReady(), Equals, false)
	s.verifyRefreshLast(c)

	checkIsAutoRefresh(c, chg.Tasks(), true)

	s.settle(c)

	// not updated
	var snapst snapstate.SnapState
	c.Assert(snapstate.Get(s.state, "some-snap", &snapst), IsNil)
	c.Check(snapst.Current, Equals, snap.Revision{N: 1})

	// updated
	c.Assert(snapstate.Get(s.state, "some-other-snap", &snapst), IsNil)
	c.Check(snapst.Current, Equals, snap.Revision{N: 11})

	c.Assert(chg.Err(), ErrorMatches, "cannot perform the following tasks:\n.*Fetch and check assertions for snap \"some-snap\" \\(11\\) \\(boom\\)")
	c.Assert(chg.IsReady(), Equals, true)

	// check-rerefresh is last
	tasks := chg.Tasks()
	checkRerefresh := tasks[len(tasks)-2]
	c.Check(checkRerefresh.Kind(), Equals, "check-rerefresh")
	c.Check(checkRerefresh.Status(), Equals, state.DoneStatus)

	c.Check(tasks[len(tasks)-1].Kind(), Equals, "mock-process-delayed-security-backend-effects")

	// validity
	c.Check(someSnapValidation, Equals, true)
}

var orthogonalAutoAliasesScenarios = []struct {
	aliasesBefore map[string][]string
	names         []string
	prune         []string
	update        bool
	new           bool
}{
	{nil, nil, nil, true, true},
	{nil, []string{"some-snap"}, nil, true, false},
	{nil, []string{"other-snap"}, nil, false, true},
	{map[string][]string{"some-snap": {"aliasA", "aliasC"}}, []string{"some-snap"}, nil, true, false},
	{map[string][]string{"other-snap": {"aliasB", "aliasC"}}, []string{"other-snap"}, []string{"other-snap"}, false, false},
	{map[string][]string{"other-snap": {"aliasB", "aliasC"}}, nil, []string{"other-snap"}, true, false},
	{map[string][]string{"other-snap": {"aliasB", "aliasC"}}, []string{"some-snap"}, nil, true, false},
	{map[string][]string{"other-snap": {"aliasC"}}, []string{"other-snap"}, []string{"other-snap"}, false, true},
	{map[string][]string{"other-snap": {"aliasC"}}, nil, []string{"other-snap"}, true, true},
	{map[string][]string{"other-snap": {"aliasC"}}, []string{"some-snap"}, nil, true, false},
	{map[string][]string{"some-snap": {"aliasB"}, "other-snap": {"aliasA"}}, []string{"some-snap"}, []string{"other-snap"}, true, false},
	{map[string][]string{"some-snap": {"aliasB"}, "other-snap": {"aliasA"}}, nil, []string{"other-snap", "some-snap"}, true, true},
	{map[string][]string{"some-snap": {"aliasB"}, "other-snap": {"aliasA"}}, []string{"other-snap"}, []string{"other-snap", "some-snap"}, false, true},
	{map[string][]string{"some-snap": {"aliasB"}}, nil, []string{"some-snap"}, true, true},
	{map[string][]string{"some-snap": {"aliasB"}}, []string{"other-snap"}, []string{"some-snap"}, false, true},
	{map[string][]string{"some-snap": {"aliasB"}}, []string{"some-snap"}, nil, true, false},
	{map[string][]string{"other-snap": {"aliasA"}}, nil, []string{"other-snap"}, true, true},
	{map[string][]string{"other-snap": {"aliasA"}}, []string{"other-snap"}, []string{"other-snap"}, false, true},
	{map[string][]string{"other-snap": {"aliasA"}}, []string{"some-snap"}, []string{"other-snap"}, true, false},
}

func (s *snapmgrTestSuite) TestUpdateManyAutoAliasesScenarios(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	s.fakeBackend.addSnapApp("some-snap", "cmdA", snap.R(4))
	s.fakeBackend.addSnapApp("other-snap", "cmdB", snap.R(2))

	snapstate.Set(s.state, "other-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "other-snap", SnapID: "other-snap-id", Revision: snap.R(2)},
		}),
		Current:  snap.R(2),
		SnapType: "app",
	})

	snapstate.AutoAliases = func(st *state.State, info *snap.Info) (map[string]string, error) {
		switch info.InstanceName() {
		case "some-snap":
			return map[string]string{"aliasA": "cmdA"}, nil
		case "other-snap":
			return map[string]string{"aliasB": "cmdB"}, nil
		}
		return nil, nil
	}

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(4)},
		}),
		Current:  snap.R(4),
		SnapType: "app",
	})

	expectedSet := func(aliases []string) map[string]bool {
		res := make(map[string]bool, len(aliases))
		for _, alias := range aliases {
			res[alias] = true
		}
		return res
	}

	for _, scenario := range orthogonalAutoAliasesScenarios {
		c.Logf("scenario start: %+v", scenario)

		for _, instanceName := range []string{"some-snap", "other-snap"} {
			var snapst snapstate.SnapState
			err := snapstate.Get(s.state, instanceName, &snapst)
			c.Assert(err, IsNil)
			snapst.Aliases = nil
			snapst.AutoAliasesDisabled = false
			if autoAliases := scenario.aliasesBefore[instanceName]; autoAliases != nil {
				targets := make(map[string]*snapstate.AliasTarget)
				for _, alias := range autoAliases {
					targets[alias] = &snapstate.AliasTarget{Auto: "cmd" + alias[len(alias)-1:]}
				}

				snapst.Aliases = targets
			}
			snapstate.Set(s.state, instanceName, &snapst)
		}

		pruneTsCnt := 0
		rerefTsCnt := 0
		delayedEffTsCnt := 0
		if scenario.update {
			// updates add re-refresh
			rerefTsCnt++
		}
		if scenario.update || scenario.new {
			// install or update would add delayed effects task
			delayedEffTsCnt++
		}
		if scenario.new && !scenario.update {
			// flattened
			delayedEffTsCnt = 0
		}
		if len(scenario.prune) != 0 {
			pruneTsCnt++
		}

		updates, tts, err := snapstate.UpdateMany(context.Background(), s.state, scenario.names, nil, s.user.ID, nil)
		c.Check(err, IsNil)
		snapTtsCnt := len(updates)
		for i, tsone := range tts {
			c.Logf("taskset[%d] kinds: %v", i, taskKinds(tsone.Tasks()))
		}
		if scenario.update {
			c.Assert(len(tts), Equals, pruneTsCnt+snapTtsCnt+rerefTsCnt+delayedEffTsCnt)
			verifyReRefreshTasks(c, tts[pruneTsCnt+snapTtsCnt])
			// do not check lanes, too much hassle
			verifyDelayedEffectsTasks(c, tts[pruneTsCnt+snapTtsCnt+1], nil, 0)
		}

		_, dropped, err := snapstate.AutoAliasesDelta(s.state, []string{"some-snap", "other-snap"})
		c.Assert(err, IsNil)

		j := 0
		expectedUpdatesSet := make(map[string]bool)
		var expectedPruned map[string]map[string]bool
		var pruneTs *state.TaskSet
		if len(scenario.prune) != 0 {
			pruneTs = tts[0]
			j++
			taskAliases := make(map[string]map[string]bool)
			for _, aliasTask := range pruneTs.Tasks() {
				c.Check(aliasTask.Kind(), Equals, "prune-auto-aliases")
				var aliases []string
				err := aliasTask.Get("aliases", &aliases)
				c.Assert(err, IsNil)
				snapsup, err := snapstate.TaskSnapSetup(aliasTask)
				c.Assert(err, IsNil)
				taskAliases[snapsup.InstanceName()] = expectedSet(aliases)
			}
			expectedPruned = make(map[string]map[string]bool)
			for _, instanceName := range scenario.prune {
				expectedPruned[instanceName] = expectedSet(dropped[instanceName])
				if instanceName == "other-snap" && !scenario.new && !scenario.update {
					expectedUpdatesSet["other-snap"] = true
				}
			}
			c.Check(taskAliases, DeepEquals, expectedPruned)
		}
		if scenario.update {
			updateTs := tts[j]
			j++
			expectedUpdatesSet["some-snap"] = true
			first := updateTs.Tasks()[0]
			c.Check(first.Kind(), Equals, "prerequisites")
			wait := false
			if expectedPruned["other-snap"]["aliasA"] {
				wait = true
			} else if expectedPruned["some-snap"] != nil {
				wait = true
			}
			if wait {
				c.Check(first.WaitTasks(), DeepEquals, pruneTs.Tasks())
			} else {
				c.Check(first.WaitTasks(), HasLen, 0)
			}
		}
		if scenario.new {
			newTs := tts[j]
			j++
			expectedUpdatesSet["other-snap"] = true
			tasks := newTs.Tasks()
			c.Check(tasks, HasLen, 1)
			aliasTask := tasks[0]
			c.Check(aliasTask.Kind(), Equals, "refresh-aliases")

			wait := false
			if expectedPruned["some-snap"]["aliasB"] {
				wait = true
			} else if expectedPruned["other-snap"] != nil {
				wait = true
			}
			if wait {
				c.Check(aliasTask.WaitTasks(), DeepEquals, pruneTs.Tasks())
			} else {
				c.Check(aliasTask.WaitTasks(), HasLen, 0)
			}
		}
		l := len(tts)
		c.Logf("len: %v j: %v", len(tts), j)
		if rerefTsCnt != 0 {
			// account for re-refresh
			l--
		}
		if delayedEffTsCnt != 0 {
			// account for delayed effects
			l--
		}
		c.Assert(j, Equals, l, Commentf("%#v", scenario))

		// check reported updated names
		c.Check(len(updates) > 0, Equals, true)
		sort.Strings(updates)
		expectedUpdates := make([]string, 0, len(expectedUpdatesSet))
		for x := range expectedUpdatesSet {
			expectedUpdates = append(expectedUpdates, x)
		}
		sort.Strings(expectedUpdates)
		c.Check(updates, DeepEquals, expectedUpdates)
	}
}

func (s *snapmgrTestSuite) TestUpdateOneAutoAliasesScenarios(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	s.fakeBackend.addSnapApp("some-snap", "cmdA", snap.R(4))
	s.fakeBackend.addSnapApp("other-snap", "cmdB", snap.R(2))
	snapstate.Set(s.state, "other-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "other-snap", SnapID: "other-snap-id", Revision: snap.R(2)},
		}),
		Current:  snap.R(2),
		SnapType: "app",
	})

	snapstate.AutoAliases = func(st *state.State, info *snap.Info) (map[string]string, error) {
		switch info.InstanceName() {
		case "some-snap":
			return map[string]string{"aliasA": "cmdA"}, nil
		case "other-snap":
			return map[string]string{"aliasB": "cmdB"}, nil
		}
		return nil, nil
	}

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(4)},
		}),
		Current:  snap.R(4),
		SnapType: "app",
	})

	expectedSet := func(aliases []string) map[string]bool {
		res := make(map[string]bool, len(aliases))
		for _, alias := range aliases {
			res[alias] = true
		}
		return res
	}

	for _, scenario := range orthogonalAutoAliasesScenarios {
		if len(scenario.names) != 1 {
			continue
		}

		for _, instanceName := range []string{"some-snap", "other-snap"} {
			var snapst snapstate.SnapState
			err := snapstate.Get(s.state, instanceName, &snapst)
			c.Assert(err, IsNil)
			snapst.Aliases = nil
			snapst.AutoAliasesDisabled = false
			if autoAliases := scenario.aliasesBefore[instanceName]; autoAliases != nil {
				targets := make(map[string]*snapstate.AliasTarget)
				for _, alias := range autoAliases {
					targets[alias] = &snapstate.AliasTarget{Auto: "cmd" + alias[len(alias)-1:]}
				}

				snapst.Aliases = targets
			}
			snapstate.Set(s.state, instanceName, &snapst)
		}

		ts, err := snapstate.Update(s.state, scenario.names[0], nil, s.user.ID, snapstate.Flags{})
		c.Assert(err, IsNil)
		_, dropped, err := snapstate.AutoAliasesDelta(s.state, []string{"some-snap", "other-snap"})
		c.Assert(err, IsNil)

		j := 0

		tasks := ts.Tasks()
		// make sure the last task from Update is the rerefresh
		if scenario.update {
			reRefresh := tasks[len(tasks)-2]
			c.Check(reRefresh.Kind(), Equals, "check-rerefresh")
			// nothing should wait on it
			c.Check(reRefresh.NumHaltTasks(), Equals, 0)

			delayedEffects := tasks[len(tasks)-1]
			c.Check(delayedEffects.Kind(), Equals, "mock-process-delayed-security-backend-effects")

			tasks = tasks[:len(tasks)-2] // and now forget about both
		}

		var expectedPruned map[string]map[string]bool
		var pruneTasks []*state.Task
		if len(scenario.prune) != 0 {
			nprune := len(scenario.prune)
			pruneTasks = tasks[:nprune]
			j += nprune
			taskAliases := make(map[string]map[string]bool)
			for _, aliasTask := range pruneTasks {
				c.Check(aliasTask.Kind(), Equals, "prune-auto-aliases")
				var aliases []string
				err := aliasTask.Get("aliases", &aliases)
				c.Assert(err, IsNil)
				snapsup, err := snapstate.TaskSnapSetup(aliasTask)
				c.Assert(err, IsNil)
				taskAliases[snapsup.InstanceName()] = expectedSet(aliases)
			}
			expectedPruned = make(map[string]map[string]bool)
			for _, instanceName := range scenario.prune {
				expectedPruned[instanceName] = expectedSet(dropped[instanceName])
			}
			c.Check(taskAliases, DeepEquals, expectedPruned)
		}
		if scenario.update {
			first := tasks[j]
			j += 20
			c.Check(first.Kind(), Equals, "prerequisites")
			wait := false
			if expectedPruned["other-snap"]["aliasA"] {
				wait = true
			} else if expectedPruned["some-snap"] != nil {
				wait = true
			}
			if wait {
				c.Check(first.WaitTasks(), DeepEquals, pruneTasks)
			} else {
				c.Check(first.WaitTasks(), HasLen, 0)
			}
		}
		if scenario.new {
			aliasTask := tasks[j]
			j++
			c.Check(aliasTask.Kind(), Equals, "refresh-aliases")
			wait := false
			if expectedPruned["some-snap"]["aliasB"] {
				wait = true
			} else if expectedPruned["other-snap"] != nil {
				wait = true
			}
			if wait {
				c.Check(aliasTask.WaitTasks(), DeepEquals, pruneTasks)
			} else {
				c.Check(aliasTask.WaitTasks(), HasLen, 0)
			}
		}
		c.Assert(len(tasks), Equals, j, Commentf("%#v", scenario))

		// conflict checks are triggered
		chg := s.state.NewChange("update", "...")
		chg.AddAll(ts)
		err = snapstate.CheckChangeConflict(s.state, scenario.names[0], nil)
		c.Check(err, ErrorMatches, `.* has "update" change in progress`)
		chg.SetStatus(state.DoneStatus)
	}
}

func (s *snapmgrTestSuite) TestUpdateLocalSnapFails(c *C) {
	si := snap.SideInfo{
		RealName: "some-snap",
		Revision: snap.R(7),
	}

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

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si}),
		Current:  si.Revision,
	})

	_, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "some-channel"}, s.user.ID, snapstate.Flags{})
	c.Assert(err, Equals, store.ErrLocalSnap)
}

func (s *snapmgrTestSuite) TestUpdateDisabledUnsupported(c *C) {
	si := snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Revision: snap.R(7),
	}

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

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:   false,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si}),
		Current:  si.Revision,
	})

	_, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "some-channel"}, s.user.ID, snapstate.Flags{})
	c.Assert(err, ErrorMatches, `refreshing disabled snap "some-snap" not supported`)
}

func (s *snapmgrTestSuite) TestUpdateKernelTrackChecksSwitchingTracks(c *C) {
	si := snap.SideInfo{
		RealName: "kernel",
		SnapID:   "kernel-id",
		Revision: snap.R(7),
	}

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

	r := snapstatetest.MockDeviceModel(ModelWithKernelTrack("18"))
	defer r()
	snapstate.Set(s.state, "kernel", &snapstate.SnapState{
		Active:          true,
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si}),
		Current:         si.Revision,
		TrackingChannel: "18/stable",
	})

	// switching tracks is not ok
	_, err := snapstate.Update(s.state, "kernel", &snapstate.RevisionOptions{Channel: "new-channel"}, s.user.ID, snapstate.Flags{})
	c.Assert(err, ErrorMatches, `cannot switch from kernel track "18" as specified for the \(device\) model to "new-channel"`)

	// no change to the channel is ok
	_, err = snapstate.Update(s.state, "kernel", nil, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)

	// switching risk level is ok
	_, err = snapstate.Update(s.state, "kernel", &snapstate.RevisionOptions{Channel: "18/beta"}, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)

	// switching just risk within the pinned track is ok
	_, err = snapstate.Update(s.state, "kernel", &snapstate.RevisionOptions{Channel: "beta"}, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)
}

func (s *snapmgrTestSuite) TestUpdateGadgetTrackChecksSwitchingTracks(c *C) {
	si := snap.SideInfo{
		RealName: "brand-gadget",
		SnapID:   "brand-gadget-id",
		Revision: snap.R(7),
	}

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

	r := snapstatetest.MockDeviceModel(ModelWithGadgetTrack("18"))
	defer r()
	snapstate.Set(s.state, "brand-gadget", &snapstate.SnapState{
		Active:          true,
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si}),
		Current:         si.Revision,
		TrackingChannel: "18/stable",
	})

	// switching tracks is not ok
	_, err := snapstate.Update(s.state, "brand-gadget", &snapstate.RevisionOptions{Channel: "new-channel"}, s.user.ID, snapstate.Flags{})
	c.Assert(err, ErrorMatches, `cannot switch from gadget track "18" as specified for the \(device\) model to "new-channel"`)

	// no change to the channel is ok
	_, err = snapstate.Update(s.state, "brand-gadget", nil, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)

	// switching risk level is ok
	_, err = snapstate.Update(s.state, "brand-gadget", &snapstate.RevisionOptions{Channel: "18/beta"}, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)

	// switching just risk within the pinned track is ok
	_, err = snapstate.Update(s.state, "brand-gadget", &snapstate.RevisionOptions{Channel: "beta"}, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)

}

func (s *snapmgrTestSuite) TestUpdateWithDeviceContextSameRevisionSwitchesChannel(c *C) {
	si := snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Revision: snap.R(7),
	}

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

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:          true,
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si}),
		TrackingChannel: "other-channel/stable",
		Current:         si.Revision,
	})

	prqt := new(testPrereqTracker)

	ts, err := snapstate.UpdateWithDeviceContext(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "channel-for-7/stable"}, s.user.ID, snapstate.Flags{}, prqt, nil, "")
	c.Assert(err, IsNil)
	c.Check(ts.Tasks(), HasLen, 1)
	c.Check(ts.Tasks()[0].Kind(), Equals, "switch-snap-channel")

	c.Assert(prqt.infos, HasLen, 1)
	c.Check(prqt.infos[0].SnapName(), Equals, "some-snap")
	c.Check(prqt.missingProviderContentTagsCalls, Equals, 1)
}

func (s *snapmgrTestSuite) TestUpdateWithDeviceContext(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	// unset the global store, it will need to come via the device context
	snapstate.ReplaceStore(s.state, nil)

	deviceCtx := &snapstatetest.TrivialDeviceContext{
		DeviceModel: DefaultModel(),
		CtxStore:    s.fakeStore,
	}

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:          true,
		TrackingChannel: "latest/edge",
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(7)}}),
		Current:         snap.R(7),
		SnapType:        "app",
	})

	validateCalled := false
	happyValidateRefreshes := func(st *state.State, refreshes []*snap.Info, ignoreValidation map[string]bool, userID int, deviceCtx1 snapstate.DeviceContext) ([]*snap.Info, error) {
		c.Check(deviceCtx1, Equals, deviceCtx)
		validateCalled = true
		return refreshes, nil
	}
	// hook it up
	snapstate.ValidateRefreshes = happyValidateRefreshes

	prqt := new(testPrereqTracker)

	ts, err := snapstate.UpdateWithDeviceContext(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "some-channel"}, s.user.ID, snapstate.Flags{}, prqt, deviceCtx, "")
	c.Assert(err, IsNil)
	verifyUpdateTasks(c, snap.TypeApp, doesReRefresh|mockDelayedEffects, 0, ts)
	c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks()))

	c.Check(validateCalled, Equals, true)

	c.Assert(prqt.infos, HasLen, 1)
	c.Check(prqt.infos[0].SnapName(), Equals, "some-snap")
	c.Check(prqt.missingProviderContentTagsCalls, Equals, 1)
}

type testPrereqTracker struct {
	infos                           []*snap.Info
	missingProviderContentTagsCalls int
}

func (prqt *testPrereqTracker) Add(info *snap.Info) {
	prqt.infos = append(prqt.infos, info)
}

func (prqt *testPrereqTracker) MissingProviderContentTags(*snap.Info, snap.InterfaceRepo) map[string][]string {
	prqt.missingProviderContentTagsCalls++
	return nil
}

func (s *snapmgrTestSuite) TestUpdatePathWithDeviceContext(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	// unset the global store, it will need to come via the device context
	snapstate.ReplaceStore(s.state, nil)

	deviceCtx := &snapstatetest.TrivialDeviceContext{
		DeviceModel: DefaultModel(),
		CtxStore:    s.fakeStore,
	}

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:          true,
		TrackingChannel: "latest/edge",
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(7)}}),
		Current:         snap.R(7),
		SnapType:        "app",
	})

	si := &snap.SideInfo{RealName: "some-snap", Revision: snap.R(8)}
	mockSnap := makeTestSnap(c, `name: some-snap
version: 1.0
epoch: 1*
`)
	prqt := new(testPrereqTracker)

	ts, err := snapstate.UpdatePathWithDeviceContext(s.state, si, mockSnap, "some-snap", &snapstate.RevisionOptions{Channel: "some-channel"}, s.user.ID, snapstate.Flags{}, prqt, deviceCtx, "")
	c.Assert(err, IsNil)
	verifyUpdateTasks(c, snap.TypeApp, doesReRefresh|localSnap|mockDelayedEffects, 0, ts)
	c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks()))
	c.Assert(prqt.infos, HasLen, 1)
	c.Check(prqt.infos[0].SnapName(), Equals, "some-snap")
	c.Check(prqt.missingProviderContentTagsCalls, Equals, 1)
}

func (s *snapmgrTestSuite) TestUpdatePathWithDeviceContextSwitchChannel(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	// unset the global store, it will need to come via the device context
	snapstate.ReplaceStore(s.state, nil)

	deviceCtx := &snapstatetest.TrivialDeviceContext{
		DeviceModel: DefaultModel(),
		CtxStore:    s.fakeStore,
	}

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:          true,
		TrackingChannel: "latest/edge",
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(7)}}),
		Current:         snap.R(7),
		SnapType:        "app",
	})

	si := &snap.SideInfo{RealName: "some-snap", Revision: snap.R(7)}
	mockSnap := makeTestSnap(c, `name: some-snap
version: 1.0
epoch: 1*
`)

	ts, err := snapstate.UpdatePathWithDeviceContext(s.state, si, mockSnap, "some-snap", &snapstate.RevisionOptions{Channel: "22/edge"}, s.user.ID, snapstate.Flags{}, nil, deviceCtx, "")
	c.Assert(err, IsNil)
	c.Check(ts.Tasks(), HasLen, 1)
	c.Check(ts.Tasks()[0].Kind(), Equals, "switch-snap-channel")
}

func (s *snapmgrTestSuite) TestUpdatePathWithDeviceContextBadFile(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	// unset the global store, it will need to come via the device context
	snapstate.ReplaceStore(s.state, nil)

	deviceCtx := &snapstatetest.TrivialDeviceContext{
		DeviceModel: DefaultModel(),
		CtxStore:    s.fakeStore,
	}

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:          true,
		TrackingChannel: "latest/edge",
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(7)}}),
		Current:         snap.R(7),
		SnapType:        "app",
	})

	si := &snap.SideInfo{RealName: "some-snap", Revision: snap.R(8)}
	path := filepath.Join(c.MkDir(), "some-snap_8.snap")
	err := os.WriteFile(path, []byte(""), 0644)
	c.Assert(err, IsNil)

	opts := &snapstate.RevisionOptions{Channel: "some-channel"}
	ts, err := snapstate.UpdatePathWithDeviceContext(s.state, si, path, "some-snap", opts, s.user.ID, snapstate.Flags{}, nil, deviceCtx, "")

	c.Assert(err, ErrorMatches, `cannot open snap file: cannot process snap or snapdir: cannot read ".*/some-snap_8.snap": EOF`)
	c.Assert(ts, IsNil)
}

func (s *snapmgrTestSuite) TestUpdateWithDeviceContextToRevision(c *C) {
	const channel = ""
	revision := snap.R(11)
	s.testUpdateWithDeviceContext(c, revision, channel)
}

func (s *snapmgrTestSuite) TestUpdateWithDeviceContextToRevisionWithChannel(c *C) {
	const channel = "some-channel"
	revision := snap.R(11)
	s.testUpdateWithDeviceContext(c, revision, channel)
}

func (s *snapmgrTestSuite) TestUpdateWithDeviceContextDefaultsToTracked(c *C) {
	const channel = ""
	revision := snap.R(0)
	s.testUpdateWithDeviceContext(c, revision, channel)
}

func (s *snapmgrTestSuite) testUpdateWithDeviceContext(c *C, revision snap.Revision, channel string) {
	s.state.Lock()
	defer s.state.Unlock()

	// unset the global store, it will need to come via the device context
	snapstate.ReplaceStore(s.state, nil)

	deviceCtx := &snapstatetest.TrivialDeviceContext{
		DeviceModel: DefaultModel(),
		CtxStore:    s.fakeStore,
	}

	const trackedChannel = "tracked-channel/stable"
	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{
				RealName: "some-snap",
				Revision: snap.R(5),
				SnapID:   "some-snap-id",
			},
		}),
		TrackingChannel: trackedChannel,
		Current:         snap.R(5),
		SnapType:        "app",
		UserID:          1,
	})

	opts := &snapstate.RevisionOptions{Channel: channel, Revision: revision}
	ts, err := snapstate.UpdateWithDeviceContext(s.state, "some-snap", opts, 0, snapstate.Flags{}, nil, deviceCtx, "")
	c.Assert(err, IsNil)
	verifyUpdateTasks(c, snap.TypeApp, doesReRefresh|mockDelayedEffects, 0, ts)
	c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks()))

	enforceValidationSets := store.SnapActionFlags(0)
	if revision.Unset() {
		enforceValidationSets = store.SnapActionEnforceValidation
	}

	expectedChannel := channel
	if revision.Unset() && channel == "" {
		expectedChannel = trackedChannel
	}

	for _, op := range s.fakeBackend.ops {
		if op.op == "storesvc-snap-action:action" {
			c.Check(op, DeepEquals, fakeOp{
				op: "storesvc-snap-action:action",
				action: store.SnapAction{
					Action:       "refresh",
					InstanceName: "some-snap",
					SnapID:       "some-snap-id",
					Revision:     revision,
					Channel:      expectedChannel,
					Flags:        enforceValidationSets,
				},
				userID: 1,
				revno:  snap.R(11),
			})
		}
	}
}

func (s *snapmgrTestSuite) TestUpdateTasksCoreSetsIgnoreOnConfigure(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	snapstate.Set(s.state, "core", &snapstate.SnapState{
		Active:          true,
		TrackingChannel: "latest/edge",
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{RealName: "core", SnapID: "core-snap-id", Revision: snap.R(7)}}),
		Current:         snap.R(7),
		SnapType:        "os",
	})

	oldConfigure := snapstate.Configure
	defer func() { snapstate.Configure = oldConfigure }()

	var configureFlags int
	snapstate.Configure = func(st *state.State, snapName string, patch map[string]any, flags int) *state.TaskSet {
		configureFlags = flags
		return state.NewTaskSet()
	}

	_, err := snapstate.Update(s.state, "core", &snapstate.RevisionOptions{Channel: "some-channel"}, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)

	// ensure the core snap sets the "ignore-hook-error" flag
	c.Check(configureFlags&snapstate.IgnoreHookError, Equals, 1)
}

func (s *snapmgrTestSuite) TestUpdateDevModeConfinementFiltering(c *C) {
	restore := maybeMockClassicSupport(c)
	defer restore()

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

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:          true,
		TrackingChannel: "channel-for-devmode/stable",
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(7)}}),
		Current:         snap.R(7),
		SnapType:        "app",
	})

	// updated snap is devmode, refresh without --devmode, do nothing
	// TODO: better error message here
	_, err := snapstate.Update(s.state, "some-snap", nil, s.user.ID, snapstate.Flags{})
	c.Assert(err, ErrorMatches, `.* requires devmode or confinement override`)

	// updated snap is devmode, refresh with --devmode
	_, err = snapstate.Update(s.state, "some-snap", nil, s.user.ID, snapstate.Flags{DevMode: true})
	c.Assert(err, IsNil)
}

func (s *snapmgrTestSuite) TestUpdateClassicConfinementFiltering(c *C) {
	restore := maybeMockClassicSupport(c)
	defer restore()

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

	snapstate.Set(s.state, "some-snap-now-classic", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{RealName: "some-snap-now-classic", SnapID: "some-snap-now-classic-id", Revision: snap.R(7)}}),
		Current:  snap.R(7),
		SnapType: "app",
	})

	// updated snap is classic, refresh without --classic, do nothing
	// TODO: better error message here
	_, err := snapstate.Update(s.state, "some-snap-now-classic", nil, s.user.ID, snapstate.Flags{})
	c.Assert(err, ErrorMatches, `.* requires classic confinement`)

	// updated snap is classic, refresh with --classic
	ts, err := snapstate.Update(s.state, "some-snap-now-classic", nil, s.user.ID, snapstate.Flags{Classic: true})
	c.Assert(err, IsNil)

	chg := s.state.NewChange("refresh", "refresh snap")
	chg.AddAll(ts)

	s.settle(c)

	c.Assert(chg.Err(), IsNil)
	c.Assert(chg.IsReady(), Equals, true)

	// verify snap is in classic
	var snapst snapstate.SnapState
	err = snapstate.Get(s.state, "some-snap-now-classic", &snapst)
	c.Assert(err, IsNil)
	c.Check(snapst.Classic, Equals, true)
}

func (s *snapmgrTestSuite) TestUpdateClassicFromClassic(c *C) {
	restore := maybeMockClassicSupport(c)
	defer restore()

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

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:          true,
		TrackingChannel: "channel-for-classic/stable",
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(7)}}),
		Current:         snap.R(7),
		SnapType:        "app",
		Flags:           snapstate.Flags{Classic: true},
	})

	// snap installed with --classic, update needs classic, refresh with --classic works
	ts, err := snapstate.Update(s.state, "some-snap", nil, s.user.ID, snapstate.Flags{Classic: true})
	c.Assert(err, IsNil)
	c.Assert(ts.Tasks(), Not(HasLen), 0)
	snapsup, err := snapstate.TaskSnapSetup(ts.Tasks()[0])
	c.Assert(err, IsNil)
	c.Check(snapsup.Flags.Classic, Equals, true)

	// devmode overrides the snapsetup classic flag
	ts, err = snapstate.Update(s.state, "some-snap", nil, s.user.ID, snapstate.Flags{DevMode: true})
	c.Assert(err, IsNil)
	c.Assert(ts.Tasks(), Not(HasLen), 0)
	snapsup, err = snapstate.TaskSnapSetup(ts.Tasks()[0])
	c.Assert(err, IsNil)
	c.Check(snapsup.Flags.Classic, Equals, false)

	// jailmode overrides it too (you need to provide both)
	ts, err = snapstate.Update(s.state, "some-snap", nil, s.user.ID, snapstate.Flags{JailMode: true})
	c.Assert(err, IsNil)
	c.Assert(ts.Tasks(), Not(HasLen), 0)
	snapsup, err = snapstate.TaskSnapSetup(ts.Tasks()[0])
	c.Assert(err, IsNil)
	c.Check(snapsup.Flags.Classic, Equals, false)

	// jailmode and classic together gets you both
	ts, err = snapstate.Update(s.state, "some-snap", nil, s.user.ID, snapstate.Flags{JailMode: true, Classic: true})
	c.Assert(err, IsNil)
	c.Assert(ts.Tasks(), Not(HasLen), 0)
	snapsup, err = snapstate.TaskSnapSetup(ts.Tasks()[0])
	c.Assert(err, IsNil)
	c.Check(snapsup.Flags.Classic, Equals, true)

	// snap installed with --classic, update needs classic, refresh without --classic works
	ts, err = snapstate.Update(s.state, "some-snap", nil, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)
	c.Assert(ts.Tasks(), Not(HasLen), 0)
	snapsup, err = snapstate.TaskSnapSetup(ts.Tasks()[0])
	c.Assert(err, IsNil)
	c.Check(snapsup.Flags.Classic, Equals, true)

	chg := s.state.NewChange("refresh", "refresh snap")
	chg.AddAll(ts)

	s.settle(c)

	// verify snap is in classic
	var snapst snapstate.SnapState
	err = snapstate.Get(s.state, "some-snap", &snapst)
	c.Assert(err, IsNil)
	c.Check(snapst.Classic, Equals, true)
}

func (s *snapmgrTestSuite) TestUpdateStrictFromClassic(c *C) {
	restore := maybeMockClassicSupport(c)
	defer restore()

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

	snapstate.Set(s.state, "some-snap-was-classic", &snapstate.SnapState{
		Active:          true,
		TrackingChannel: "channel/stable",
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{RealName: "some-snap-was-classic", SnapID: "some-snap-was-classic-id", Revision: snap.R(7)}}),
		Current:         snap.R(7),
		SnapType:        "app",
		Flags:           snapstate.Flags{Classic: true},
	})

	// snap installed with --classic, update does not need classic, refresh works without --classic
	_, err := snapstate.Update(s.state, "some-snap-was-classic", nil, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)

	// snap installed with --classic, update does not need classic, refresh works with --classic
	_, err = snapstate.Update(s.state, "some-snap-was-classic", nil, s.user.ID, snapstate.Flags{Classic: true})
	c.Assert(err, IsNil)
}

func (s *snapmgrTestSuite) TestUpdateChannelFallback(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:          true,
		TrackingChannel: "latest/edge",
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(7)}}),
		Current:         snap.R(7),
		SnapType:        "app",
	})

	ts, err := snapstate.Update(s.state, "some-snap", nil, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)

	var snapsup snapstate.SnapSetup
	err = ts.Tasks()[0].Get("snap-setup", &snapsup)
	c.Assert(err, IsNil)

	c.Check(snapsup.Channel, Equals, "latest/edge")
}

func (s *snapmgrTestSuite) TestUpdateTooEarly(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

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

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(7)}}),
		Current:  snap.R(7),
		SnapType: "app",
	})

	_, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "some-channel"}, s.user.ID, snapstate.Flags{})
	c.Check(err, FitsTypeOf, &snapstate.ChangeConflictError{})
	c.Assert(err, ErrorMatches, `too early for operation, device not yet seeded or device model not acknowledged`)
}

func (s *snapmgrTestSuite) TestUpdateConflict(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(7)}}),
		Current:  snap.R(7),
		SnapType: "app",
	})

	ts, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "some-channel"}, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)
	// need a change to make the tasks visible
	s.state.NewChange("refresh", "...").AddAll(ts)

	_, err = snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "some-channel"}, s.user.ID, snapstate.Flags{})
	c.Assert(err, ErrorMatches, `snap "some-snap" has "refresh" change in progress`)
}

func (s *snapmgrTestSuite) TestUpdateCreatesGCTasks(c *C) {
	restore := release.MockOnClassic(false)
	defer restore()

	s.testUpdateCreatesGCTasks(c, 2)
}

func (s *snapmgrTestSuite) TestUpdateCreatesGCTasksOnClassic(c *C) {
	restore := release.MockOnClassic(true)
	defer restore()

	s.testUpdateCreatesGCTasks(c, 3)
}

func (s *snapmgrTestSuite) testUpdateCreatesGCTasks(c *C, expectedDiscards int) {
	s.state.Lock()
	defer s.state.Unlock()

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)},
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(2)},
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(3)},
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(4)},
		}),
		Current:  snap.R(4),
		SnapType: "app",
	})

	ts, err := snapstate.Update(s.state, "some-snap", nil, 0, snapstate.Flags{})
	c.Assert(err, IsNil)

	// ensure edges information is still there
	te, err := ts.Edge(snapstate.LastBeforeLocalModificationsEdge)
	c.Assert(te, NotNil)
	c.Assert(err, IsNil)

	verifyUpdateTasks(c, snap.TypeApp, doesReRefresh|mockDelayedEffects, expectedDiscards, ts)
	c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks()))
}

func (s *snapmgrTestSuite) TestUpdateCreatesDiscardAfterCurrentTasks(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)},
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(2)},
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(3)},
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(4)},
		}),
		Current:  snap.R(1),
		SnapType: "app",
	})

	ts, err := snapstate.Update(s.state, "some-snap", nil, 0, snapstate.Flags{})
	c.Assert(err, IsNil)

	verifyUpdateTasks(c, snap.TypeApp, doesReRefresh|mockDelayedEffects, 3, ts)
	c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks()))
}

func (s *snapmgrTestSuite) TestUpdateManyTooEarly(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

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

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(7)}}),
		Current:  snap.R(7),
		SnapType: "app",
	})

	_, _, err := snapstate.UpdateMany(context.Background(), s.state, nil, nil, 0, nil)
	c.Check(err, FitsTypeOf, &snapstate.ChangeConflictError{})
	c.Assert(err, ErrorMatches, `too early for operation, device not yet seeded or device model not acknowledged`)
}

func (s *snapmgrTestSuite) TestUpdateMany(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)},
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(2)},
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(3)},
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(4)},
		}),
		Current:  snap.R(1),
		SnapType: "app",
	})

	updates, tts, err := snapstate.UpdateMany(context.Background(), s.state, nil, nil, 0, nil)
	c.Assert(err, IsNil)
	c.Assert(tts, HasLen, 3)
	verifyReRefreshTasks(c, tts[1])
	c.Check(updates, DeepEquals, []string{"some-snap"})

	ts := tts[0]
	verifyUpdateTasks(c, snap.TypeApp, 0, 3, ts)

	// check that the tasks are in non-default lane
	for _, t := range ts.Tasks() {
		c.Assert(t.Lanes(), DeepEquals, []int{1})
	}
	// 1==rerefresh, 2==mock-process-delayed-backend-effects
	c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks())+2)

	// ensure edges information is still there
	te, err := ts.Edge(snapstate.LastBeforeLocalModificationsEdge)
	c.Assert(te, NotNil)
	c.Assert(err, IsNil)

	checkIsAutoRefresh(c, ts.Tasks(), false)
}

func (s *snapmgrTestSuite) TestUpdateManyIgnoreRunning(c *C) {
	si := snap.SideInfo{
		RealName: "some-snap",
		Revision: snap.R(1),
		SnapID:   "some-snap-id",
	}
	snaptest.MockSnap(c, `name: some-snap`, &si)

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

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:          true,
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si}),
		Current:         si.Revision,
		SnapType:        "app",
		TrackingChannel: "latest/stable",
	})

	updates, tts, err := snapstate.UpdateMany(context.Background(), s.state,
		[]string{"some-snap"}, nil, 0, &snapstate.Flags{IgnoreRunning: true})
	c.Assert(err, IsNil)
	c.Assert(tts, HasLen, 3)
	verifyReRefreshTasks(c, tts[1])
	c.Assert(updates, HasLen, 1)

	snapsup, err := snapstate.TaskSnapSetup(tts[0].Tasks()[0])
	c.Assert(err, IsNil)
	c.Assert(snapsup.IgnoreRunning, Equals, true)
}

func (s *snapmgrTestSuite) TestUpdateManyTransactionally(c *C) {
	si := snap.SideInfo{
		RealName: "some-snap",
		Revision: snap.R(1),
		SnapID:   "some-snap-id",
	}
	snaptest.MockSnap(c, `name: some-snap`, &si)
	si2 := snap.SideInfo{
		RealName: "some-other-snap",
		Revision: snap.R(1),
		SnapID:   "some-other-snap-id",
	}
	snaptest.MockSnap(c, `name: some-other-snap`, &si2)

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

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:          true,
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si}),
		Current:         si.Revision,
		SnapType:        "app",
		TrackingChannel: "latest/stable",
	})

	snapstate.Set(s.state, "some-other-snap", &snapstate.SnapState{
		Active:          true,
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si2}),
		Current:         si2.Revision,
		SnapType:        "app",
		TrackingChannel: "latest/stable",
	})

	updates, tts, err := snapstate.UpdateMany(context.Background(), s.state,
		[]string{"some-snap", "some-other-snap"}, nil, 0,
		&snapstate.Flags{Transaction: client.TransactionAllSnaps})
	c.Assert(err, IsNil)
	c.Assert(tts, HasLen, 4)
	verifyReRefreshTasks(c, tts[2])
	verifyDelayedEffectsTasks(c, tts[3], []int{1}, 1)
	c.Assert(updates, HasLen, 2)

	// Last task is re-refresh, so it is a different lane
	for _, ts := range tts[:len(tts)-2] {
		checkIsAutoRefresh(c, ts.Tasks(), false)
		// check that tasksets are all in one lane
		for _, t := range ts.Tasks() {
			c.Assert(t.Lanes(), DeepEquals, []int{1})
		}
	}
}

func (s *snapmgrTestSuite) TestUpdateManyTransactionallyFails(c *C) {
	restore := release.MockOnClassic(true)
	defer restore()

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

	// trigger download error on one of the snaps
	s.fakeStore.downloadError["some-other-snap"] = fmt.Errorf("boom")

	snapstate.ReplaceStore(s.state,
		contentStore{fakeStore: s.fakeStore, state: s.state})

	si := &snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Revision: snap.R(7),
	}
	snaptest.MockSnap(c, `name: some-snap`, si)
	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:          true,
		TrackingChannel: "latest/edge",
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
		Current:         snap.R(7),
		SnapType:        "app",
	})
	snapstate.Set(s.state, "some-other-snap", &snapstate.SnapState{
		Active:          true,
		TrackingChannel: "latest/stable",
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{
			RealName: "some-other-snap",
			SnapID:   "some-other-snap-id",
			Revision: snap.R(1),
		}}),
		Current:  snap.R(1),
		SnapType: "app",
	})

	chg := s.state.NewChange("refresh", "refresh some snaps")
	updated, tts, err := snapstate.UpdateMany(context.Background(), s.state,
		[]string{"some-snap", "some-other-snap"}, nil, 0,
		&snapstate.Flags{Transaction: client.TransactionAllSnaps})
	c.Assert(err, IsNil)
	c.Check(updated, testutil.DeepUnsortedMatches,
		[]string{"some-snap", "some-other-snap"})
	for _, ts := range tts {
		chg.AddAll(ts)
	}

	s.settle(c)

	// content consumer snap fails to download
	c.Assert(chg.Err(), ErrorMatches, "cannot perform the following tasks:\n.*Download snap \"some-other-snap\" \\(11\\) from channel \"latest/stable\" \\(boom\\).*")
	c.Assert(chg.IsReady(), Equals, true)

	var snapSt snapstate.SnapState
	// some-other-snap not updated due to download failure
	c.Assert(snapstate.Get(s.state, "some-other-snap", &snapSt), IsNil)
	c.Check(snapSt.Current, Equals, snap.R(1))

	// some-snap not updated either as this is a transactional refresh
	// (on update revision is 11)
	c.Assert(snapstate.Get(s.state, "some-snap", &snapSt), IsNil)
	c.Check(snapSt.Current, Equals, snap.R(7))
}

func (s *snapmgrTestSuite) TestUpdateManyFailureDoesntUndoSnapdRefresh(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	r := snapstatetest.MockDeviceModel(ModelWithBase("core18"))
	defer r()

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)},
		}),
		Current:         snap.R(1),
		SnapType:        "app",
		TrackingChannel: "channel-for-base/stable",
	})

	snapstate.Set(s.state, "core18", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "core18", SnapID: "core18-snap-id", Revision: snap.R(1)},
		}),
		Current:  snap.R(1),
		SnapType: "base",
	})

	snapstate.Set(s.state, "some-base", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-base", SnapID: "some-base-id", Revision: snap.R(1)},
		}),
		Current:  snap.R(1),
		SnapType: "base",
	})

	snapstate.Set(s.state, "snapd", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "snapd", SnapID: "snapd-snap-id", Revision: snap.R(1)},
		}),
		Current:  snap.R(1),
		SnapType: "app",
	})

	updates, tts, err := snapstate.UpdateMany(context.Background(), s.state, []string{"some-snap", "some-base", "snapd"}, nil, 0, nil)
	c.Assert(err, IsNil)
	c.Assert(tts, HasLen, 5)
	c.Assert(updates, HasLen, 3)

	chg := s.state.NewChange("refresh", "...")
	for _, ts := range tts {
		chg.AddAll(ts)
	}

	// refresh of some-snap fails on link-snap
	s.fakeBackend.linkSnapFailTrigger = filepath.Join(dirs.SnapMountDir, "/some-snap/11")

	// up until daemon restart caused by snapd installation
	s.settle(c)
	kind := restart.Pending(s.state)
	c.Check(kind, Equals, restart.RestartDaemon)
	restart.MockPending(s.state, restart.RestartUnset)
	// run through the end
	s.settle(c)

	c.Check(chg.Err(), ErrorMatches, ".*cannot perform the following tasks:\n- Make snap \"some-snap\" \\(11\\) available to the system.*")
	c.Check(chg.IsReady(), Equals, true)

	var snapst snapstate.SnapState

	// failed snap remains at the old revision, snapd and some-base are refreshed.
	c.Assert(snapstate.Get(s.state, "some-snap", &snapst), IsNil)
	c.Check(snapst.Current, Equals, snap.Revision{N: 1})

	c.Assert(snapstate.Get(s.state, "snapd", &snapst), IsNil)
	c.Check(snapst.Current, Equals, snap.Revision{N: 11})

	c.Assert(snapstate.Get(s.state, "some-base", &snapst), IsNil)
	c.Check(snapst.Current, Equals, snap.Revision{N: 11})

	var undoneDownloads, doneDownloads int
	for _, ts := range tts {
		for _, t := range ts.Tasks() {
			if t.Kind() == "download-snap" {
				sup, err := snapstate.TaskSnapSetup(t)
				c.Assert(err, IsNil)
				switch sup.SnapName() {
				case "some-snap":
					undoneDownloads++
					c.Check(t.Status(), Equals, state.UndoneStatus)
				case "snapd", "some-base":
					doneDownloads++
					c.Check(t.Status(), Equals, state.DoneStatus)
				default:
					c.Errorf("unexpected snap %s", sup.SnapName())
				}
			}
		}
	}
	c.Assert(undoneDownloads, Equals, 1)
	c.Assert(doneDownloads, Equals, 2)
}

func (s *snapmgrTestSuite) TestUpdateManyDevModeConfinementFiltering(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:          true,
		TrackingChannel: "channel-for-devmode/stable",
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(7)}}),
		Current:         snap.R(7),
		SnapType:        "app",
	})

	// updated snap is devmode, updatemany doesn't update it
	_, tts, _ := snapstate.UpdateMany(context.Background(), s.state, []string{"some-snap"}, nil, s.user.ID, nil)
	// FIXME: UpdateMany will not error out in this case (daemon catches this case, with a weird error)
	c.Assert(tts, HasLen, 0)
}

func (s *snapmgrTestSuite) TestUpdateManyClassicConfinementFiltering(c *C) {
	restore := maybeMockClassicSupport(c)
	defer restore()

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

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:          true,
		TrackingChannel: "channel-for-classic/stable",
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(7)}}),
		Current:         snap.R(7),
		SnapType:        "app",
	})

	// if a snap installed without --classic gets a classic update it isn't installed
	_, tts, _ := snapstate.UpdateMany(context.Background(), s.state, []string{"some-snap"}, nil, s.user.ID, nil)
	// FIXME: UpdateMany will not error out in this case (daemon catches this case, with a weird error)
	c.Assert(tts, HasLen, 0)
}

func (s *snapmgrTestSuite) TestUpdateManyClassic(c *C) {
	restore := maybeMockClassicSupport(c)
	defer restore()

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

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:          true,
		TrackingChannel: "channel-for-classic/stable",
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(7)}}),
		Current:         snap.R(7),
		SnapType:        "app",
		Flags:           snapstate.Flags{Classic: true},
	})

	// snap installed with classic: refresh gets classic
	_, tts, err := snapstate.UpdateMany(context.Background(), s.state, []string{"some-snap"}, nil, s.user.ID, nil)
	c.Assert(err, IsNil)
	c.Assert(tts, HasLen, 3)
	verifyReRefreshTasks(c, tts[1])
}

func (s *snapmgrTestSuite) TestUpdateManyClassicToStrict(c *C) {
	restore := maybeMockClassicSupport(c)
	defer restore()

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

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:          true,
		TrackingChannel: "stable",
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(7)}}),
		Current:         snap.R(7),
		SnapType:        "app",
		Flags:           snapstate.Flags{Classic: true},
	})

	// snap installed with classic: refresh gets classic
	_, tts, err := snapstate.UpdateMany(context.Background(), s.state, []string{"some-snap"}, nil, s.user.ID, &snapstate.Flags{Classic: true})
	c.Assert(err, IsNil)
	c.Assert(tts, HasLen, 3)
	// ensure we clear the classic flag
	snapsup, err := snapstate.TaskSnapSetup(tts[0].Tasks()[0])
	c.Assert(err, IsNil)
	c.Assert(snapsup.Flags.Classic, Equals, false)

	verifyReRefreshTasks(c, tts[1])
}

func (s *snapmgrTestSuite) TestUpdateManyDevMode(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active: true,
		Flags:  snapstate.Flags{DevMode: true},
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)},
		}),
		Current:  snap.R(1),
		SnapType: "app",
	})

	updates, _, err := snapstate.UpdateMany(context.Background(), s.state, []string{"some-snap"}, nil, 0, nil)
	c.Assert(err, IsNil)
	c.Check(updates, HasLen, 1)
}

func (s *snapmgrTestSuite) TestUpdateManyOneSwitchesChannel(c *C) {
	sideInfos := []snap.SideInfo{
		{
			RealName: "some-snap",
			Revision: snap.R(7),
			SnapID:   "some-snap-id",
		},
		{
			RealName: "some-other-snap",
			Revision: snap.R(1),
			SnapID:   "some-other-snap-id",
		},
	}

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

	updates := make([]snapstate.StoreUpdate, 0, len(sideInfos))
	for _, si := range sideInfos {
		snaptest.MockSnap(c, fmt.Sprintf("name: %s", si.RealName), &si)
		snapstate.Set(s.state, si.RealName, &snapstate.SnapState{
			Active:          true,
			Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si}),
			Current:         si.Revision,
			SnapType:        "app",
			TrackingChannel: "latest/stable",
		})

		// for some-snap, we're going to switch to a different channel
		var channel string
		if si.RealName == "some-snap" {
			channel = "channel-for-7/stable"
		}

		updates = append(updates, snapstate.StoreUpdate{
			InstanceName: si.RealName,
			RevOpts: snapstate.RevisionOptions{
				Channel: channel,
			},
		})
	}

	goal := snapstate.StoreUpdateGoal(updates...)
	names, uts, err := snapstate.UpdateWithGoal(context.Background(), s.state, goal, nil, snapstate.Options{})
	c.Assert(err, IsNil)
	c.Assert(uts.Refresh, HasLen, 4)

	c.Assert(names, testutil.DeepUnsortedMatches, []string{"some-snap", "some-other-snap"})

	verifyReRefreshTasks(c, uts.Refresh[2])
	// snap is update in default lane 0
	verifyDelayedEffectsTasks(c, uts.Refresh[3], []int{0}, 0)
	c.Assert(uts.Refresh[1].Tasks(), HasLen, 1)

	switchTask := uts.Refresh[1].Tasks()[0]
	c.Check(switchTask.Kind(), Equals, "switch-snap-channel")
}

func (s *snapmgrTestSuite) TestUpdateManyModelBaseSwitchesChannelUpdateCertDB(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	restore := snapstatetest.MockDeviceModel(ModelWithBase("core18"))
	defer restore()

	si := snap.SideInfo{
		RealName: "core18",
		Revision: snap.R(1),
		SnapID:   "core18-snap-id",
	}

	snaptest.MockSnap(c, "name: core18\ntype: base", &si)
	snapstate.Set(s.state, "core18", &snapstate.SnapState{
		Active:          true,
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si}),
		Current:         si.Revision,
		SnapType:        "base",
		TrackingChannel: "latest/stable",
	})

	goal := snapstate.StoreUpdateGoal(snapstate.StoreUpdate{
		InstanceName: "core18",
		RevOpts: snapstate.RevisionOptions{
			Channel: "channel-for-1/stable",
		},
	})

	names, uts, err := snapstate.UpdateWithGoal(context.Background(), s.state, goal, nil, snapstate.Options{})
	c.Assert(err, IsNil)
	c.Assert(names, DeepEquals, []string{"core18"})

	updateCertDBTasks := 0
	for _, ts := range uts.Refresh {
		for _, t := range ts.Tasks() {
			if t.Kind() == "update-cert-db" {
				updateCertDBTasks++
			}
		}
	}
	c.Check(updateCertDBTasks, Equals, 1)
}

func (s *snapmgrTestSuite) TestUpdateManyNonModelBaseRefreshNoUpdateCertDB(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	restore := snapstatetest.MockDeviceModel(ModelWithBase("core18"))
	defer restore()

	siBootBase := snap.SideInfo{
		RealName: "core18",
		Revision: snap.R(1),
		SnapID:   "core18-snap-id",
	}
	snaptest.MockSnap(c, "name: core18\ntype: base", &siBootBase)
	snapstate.Set(s.state, "core18", &snapstate.SnapState{
		Active:          true,
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&siBootBase}),
		Current:         siBootBase.Revision,
		SnapType:        "base",
		TrackingChannel: "latest/stable",
	})

	siOtherBase := snap.SideInfo{
		RealName: "some-base",
		Revision: snap.R(1),
		SnapID:   "some-base-id",
	}
	snaptest.MockSnap(c, "name: some-base\ntype: base", &siOtherBase)
	snapstate.Set(s.state, "some-base", &snapstate.SnapState{
		Active:          true,
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&siOtherBase}),
		Current:         siOtherBase.Revision,
		SnapType:        "base",
		TrackingChannel: "latest/stable",
	})

	updates, uts, err := snapstate.UpdateMany(context.Background(), s.state, []string{"some-base"}, nil, 0, nil)
	c.Assert(err, IsNil)
	c.Assert(updates, DeepEquals, []string{"some-base"})

	for _, ts := range uts {
		for _, t := range ts.Tasks() {
			c.Check(t.Kind(), Not(Equals), "update-cert-db")
		}
	}
}

func (s *snapmgrTestSuite) TestUpdateManyOneSwitchesChannelWithAutoAlias(c *C) {
	sideInfos := []snap.SideInfo{
		{
			RealName: "alias-snap",
			Revision: snap.R(11),
			SnapID:   "alias-snap-id",
		},
		{
			RealName: "some-other-snap",
			Revision: snap.R(1),
			SnapID:   "some-other-snap-id",
		},
	}

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

	n := 0
	snapstate.AutoAliases = func(st *state.State, info *snap.Info) (map[string]string, error) {
		if info.InstanceName() == "alias-snap" {
			if n > 0 {
				return map[string]string{
					"alias1": "cmd1",
					"alias2": "cmd2",
				}, nil
			}
			n++
		}
		return nil, nil
	}

	updates := make([]snapstate.StoreUpdate, 0, len(sideInfos))
	for _, si := range sideInfos {
		snaptest.MockSnap(c, fmt.Sprintf("name: %s", si.RealName), &si)
		snapstate.Set(s.state, si.RealName, &snapstate.SnapState{
			Active:          true,
			Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si}),
			Current:         si.Revision,
			SnapType:        "app",
			TrackingChannel: "latest/stable",
		})

		// for alias-snap, we're going to switch to a different channel
		var channel string
		if si.RealName == "alias-snap" {
			channel = "latest/candidate"
		}

		updates = append(updates, snapstate.StoreUpdate{
			InstanceName: si.RealName,
			RevOpts: snapstate.RevisionOptions{
				Channel: channel,
			},
		})
	}

	s.state.Set("aliases", map[string]map[string]string{
		"alias-snap": {
			"alias1": "auto",
		},
	})

	s.state.Unlock()
	err := s.snapmgr.Ensure()
	s.state.Lock()
	c.Assert(err, IsNil)

	goal := snapstate.StoreUpdateGoal(updates...)
	names, uts, err := snapstate.UpdateWithGoal(context.Background(), s.state, goal, nil, snapstate.Options{})
	c.Assert(err, IsNil)
	// switch channel task set, refresh task set, auto alias task set, rerefresh
	// task set, and delayed effects task set
	c.Assert(uts.Refresh, HasLen, 5)
	c.Assert(names, testutil.DeepUnsortedMatches, []string{"alias-snap", "some-other-snap"})

	verifyReRefreshTasks(c, uts.Refresh[3])
	verifyDelayedEffectsTasks(c, uts.Refresh[4], []int{0}, 0)

	switchTask := uts.Refresh[2].Tasks()[0]
	c.Assert(switchTask.Kind(), Equals, "switch-snap-channel")
	c.Assert(taskKinds(uts.Refresh[1].Tasks()), DeepEquals, []string{"refresh-aliases"})
}

func (s *snapmgrTestSuite) TestUpdateAllDevMode(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active: true,
		Flags:  snapstate.Flags{DevMode: true},
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)},
		}),
		Current:  snap.R(1),
		SnapType: "app",
	})

	updates, _, err := snapstate.UpdateMany(context.Background(), s.state, nil, nil, 0, nil)
	c.Assert(err, IsNil)
	c.Check(updates, HasLen, 0)
}

func taskSetsShareLane(tss ...*state.TaskSet) bool {
	lanes := make(map[int]int)
	for _, ts := range tss {
		// use a known task to read the lanes from, where expect
		// the task to be in a shared task-lane across the provided
		// task-sets
		for _, t := range ts.Tasks() {
			if t.Kind() == "link-snap" {
				for _, l := range t.Lanes() {
					lanes[l]++
				}
				break
			}
		}
	}
	// Now all of the lanes in the map should have the
	// value of the len(tss), as that would indicate that
	// link-snap tasks of each task-set have increased that lane.
	for _, c := range lanes {
		if c != len(tss) {
			return false
		}
	}
	return true
}

func (s *snapmgrTestSuite) TestUpdateManyWaitForBasesUC16(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

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

	snapstate.Set(s.state, "some-base", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-base", SnapID: "some-base-id", Revision: snap.R(1)},
		}),
		Current:  snap.R(1),
		SnapType: "base",
	})

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)},
		}),
		Current:         snap.R(1),
		SnapType:        "app",
		TrackingChannel: "channel-for-base/stable",
	})

	updates, tts, err := snapstate.UpdateMany(context.Background(), s.state, []string{"some-snap", "core", "some-base"}, nil, 0, nil)
	c.Assert(err, IsNil)
	c.Assert(tts, HasLen, 5)
	verifyReRefreshTasks(c, tts[3])
	verifyDelayedEffectsTasks(c, tts[4], []int{1, 2, 3}, 0)
	c.Check(updates, HasLen, 3)

	// to make TaskSnapSetup work
	chg := s.state.NewChange("refresh", "...")
	for _, ts := range tts {
		chg.AddAll(ts)
	}

	// Some-snap is expected to wait for both the essential snap, but
	// also the base of some-snap. These dependencies are set up between
	// last tasks of core/some-base, on preprequisites
	lastTaskOfCore, err := tts[0].Edge(snapstate.EndEdge)
	c.Assert(err, IsNil)
	lastTaskOfBase, err := tts[1].Edge(snapstate.EndEdge)
	c.Assert(err, IsNil)
	firstTaskOfSnap, err := tts[2].Edge(snapstate.BeginEdge)
	c.Assert(err, IsNil)
	c.Check(firstTaskOfSnap.WaitTasks(), HasLen, 2)
	c.Check(firstTaskOfSnap.WaitTasks(), testutil.Contains, lastTaskOfCore)
	c.Check(firstTaskOfSnap.WaitTasks(), testutil.Contains, lastTaskOfBase)

	// core and the other snaps are not expected to share the same lane
	c.Check(taskSetsShareLane(tts[0], tts[1]), Equals, false)
	c.Check(taskSetsShareLane(tts[0], tts[2]), Equals, false)
	c.Check(taskSetsShareLane(tts[1], tts[2]), Equals, false)

	// Manually verify their lanes
	c.Check(tts[0].Tasks()[0].Lanes(), DeepEquals, []int{1})
	c.Check(tts[1].Tasks()[0].Lanes(), DeepEquals, []int{2})
	c.Check(tts[2].Tasks()[0].Lanes(), DeepEquals, []int{3})
}

func (s *snapmgrTestSuite) TestUpdateManyWaitForBasesUC18(c *C) {
	r := snapstatetest.MockDeviceModel(ModelWithBase("core18"))
	defer r()

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

	snapstate.Set(s.state, "core18", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "core18", SnapID: "core18-snap-id", Revision: snap.R(1)},
		}),
		Current:  snap.R(1),
		SnapType: "base",
	})

	snapstate.Set(s.state, "some-base", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-base", SnapID: "some-base-id", Revision: snap.R(1)},
		}),
		Current:  snap.R(1),
		SnapType: "base",
	})

	snapstate.Set(s.state, "snapd", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "snapd", SnapID: "snapd-snap-id", Revision: snap.R(1)},
		}),
		Current:  snap.R(1),
		SnapType: "app",
	})

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)},
		}),
		Current:         snap.R(1),
		SnapType:        "app",
		TrackingChannel: "channel-for-base/stable",
	})

	updates, tts, err := snapstate.UpdateMany(context.Background(), s.state, []string{"some-snap", "core18", "some-base", "snapd"}, nil, 0, nil)
	c.Assert(err, IsNil)
	c.Assert(tts, HasLen, 6)
	verifyReRefreshTasks(c, tts[4])
	verifyDelayedEffectsTasks(c, tts[5], []int{1, 2, 3, 4}, 0)
	c.Check(updates, HasLen, 4)

	// to make TaskSnapSetup work
	chg := s.state.NewChange("refresh", "...")
	for _, ts := range tts {
		chg.AddAll(ts)
	}

	// Some-app will be waiting for the bases, which includes both some-base and
	// core18. The first task of some-snap will be waiting for the last tasks of
	// those two dependencies.
	lastTaskOfCore, err := tts[1].Edge(snapstate.EndEdge)
	c.Assert(err, IsNil)
	lastTaskOfBase, err := tts[2].Edge(snapstate.EndEdge)
	c.Assert(err, IsNil)
	firstTaskOfSnap, err := tts[3].Edge(snapstate.BeginEdge)
	c.Assert(err, IsNil)
	c.Check(firstTaskOfSnap.WaitTasks(), HasLen, 2)
	c.Check(firstTaskOfSnap.WaitTasks(), testutil.Contains, lastTaskOfCore)
	c.Check(firstTaskOfSnap.WaitTasks(), testutil.Contains, lastTaskOfBase)

	// Core18 and snapd are not expected to share the same lane, we only
	// check essential snaps as those are the ones that can end up in same lane.
	// Although snapd should never be a part of the transactional lane.
	c.Check(taskSetsShareLane(tts[0], tts[1]), Equals, false)

	// Manually verify the lanes of the initial task for the 4 task-sets
	c.Check(tts[0].Tasks()[0].Lanes(), DeepEquals, []int{1}) // snapd
	c.Check(tts[1].Tasks()[0].Lanes(), DeepEquals, []int{2}) // core18
	c.Check(tts[2].Tasks()[0].Lanes(), DeepEquals, []int{3}) // base
	c.Check(tts[3].Tasks()[0].Lanes(), DeepEquals, []int{4}) // snap
}

func (s *validationSetsSuite) TestUpdateManyWithRevisionOpts(c *C) {
	// current validation set forbids "some-snap"
	vsets := snapasserts.NewValidationSets()
	someSnapConstraint := map[string]any{
		"id":       "yOqKhntON3vR7kwEbVPsILm7bUViPDzx",
		"name":     "some-snap",
		"presence": "required",
		"required": "1",
	}
	bar := s.mockValidationSetAssert(c, "bar", "1", someSnapConstraint)
	err := vsets.Add(bar.(*asserts.ValidationSet))
	c.Assert(err, IsNil)

	restore := snapstate.MockEnforcedValidationSets(func(st *state.State, extraVss ...*asserts.ValidationSet) (*snapasserts.ValidationSets, error) {
		return vsets, nil
	})
	defer restore()

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

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)},
		}),
		Current:  snap.R(1),
		SnapType: "app",
	})

	tr := assertstate.ValidationSetTracking{
		AccountID: "foo",
		Name:      "bar",
		Mode:      assertstate.Enforce,
		Current:   1,
	}
	assertstate.UpdateValidationSet(s.state, &tr)

	// updating "some-snap" with revision opts should succeed because current
	// validation sets should be ignored
	revOpts := []*snapstate.RevisionOptions{{Revision: snap.R(2), ValidationSets: vsets}}
	affected, tss, err := snapstate.UpdateMany(context.Background(), s.state, []string{"some-snap"}, revOpts, 0, nil)
	c.Assert(err, IsNil)
	c.Assert(affected, DeepEquals, []string{"some-snap"})

	chg := s.state.NewChange("refresh", "")
	for _, ts := range tss {
		chg.AddAll(ts)
	}

	s.settle(c)
	c.Check(chg.Err(), IsNil)
	c.Check(chg.Status(), Equals, state.DoneStatus)
}

func (s *snapmgrTestSuite) TestUpdateManyValidateRefreshes(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)},
		}),
		Current:  snap.R(1),
		SnapType: "app",
	})

	validateCalled := false
	validateRefreshes := func(st *state.State, refreshes []*snap.Info, ignoreValidation map[string]bool, userID int, deviceCtx snapstate.DeviceContext) ([]*snap.Info, error) {
		validateCalled = true
		c.Check(refreshes, HasLen, 1)
		c.Check(refreshes[0].InstanceName(), Equals, "some-snap")
		c.Check(refreshes[0].SnapID, Equals, "some-snap-id")
		c.Check(refreshes[0].Revision, Equals, snap.R(11))
		c.Check(ignoreValidation, HasLen, 0)
		return refreshes, nil
	}
	// hook it up
	snapstate.ValidateRefreshes = validateRefreshes

	updates, tts, err := snapstate.UpdateMany(context.Background(), s.state, nil, nil, 0, nil)
	c.Assert(err, IsNil)
	c.Assert(tts, HasLen, 3)
	verifyReRefreshTasks(c, tts[1])
	verifyDelayedEffectsTasks(c, tts[2], []int{1}, 0)
	c.Check(updates, DeepEquals, []string{"some-snap"})
	verifyUpdateTasks(c, snap.TypeApp, 0, 0, tts[0])

	c.Check(validateCalled, Equals, true)
}

func (s *snapmgrTestSuite) TestParallelInstanceUpdateMany(c *C) {
	restore := release.MockOnClassic(false)
	defer restore()

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

	tr := config.NewTransaction(s.state)
	tr.Set("core", "experimental.parallel-instances", true)
	tr.Commit()

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)},
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(2)},
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(3)},
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(4)},
		}),
		Current:  snap.R(1),
		SnapType: "app",
	})
	snapstate.Set(s.state, "some-snap_instance", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)},
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(2)},
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(3)},
		}),
		Current:     snap.R(3),
		SnapType:    "app",
		InstanceKey: "instance",
	})

	updates, tts, err := snapstate.UpdateMany(context.Background(), s.state, nil, nil, 0, nil)
	c.Assert(err, IsNil)
	c.Assert(tts, HasLen, 4)
	verifyReRefreshTasks(c, tts[2])
	verifyDelayedEffectsTasks(c, tts[3], []int{1, 2}, 0)
	// ensure stable ordering of updates list
	if updates[0] != "some-snap" {
		updates[1], updates[0] = updates[0], updates[1]
	}

	c.Check(updates, DeepEquals, []string{"some-snap", "some-snap_instance"})

	var snapsup, snapsupInstance *snapstate.SnapSetup

	// ensure stable ordering of task sets list
	snapsup, err = snapstate.TaskSnapSetup(tts[0].Tasks()[0])
	c.Assert(err, IsNil)
	if snapsup.InstanceName() != "some-snap" {
		tts[0], tts[1] = tts[1], tts[0]
		snapsup, err = snapstate.TaskSnapSetup(tts[0].Tasks()[0])
		c.Assert(err, IsNil)
	}
	snapsupInstance, err = snapstate.TaskSnapSetup(tts[1].Tasks()[0])
	c.Assert(err, IsNil)

	c.Assert(snapsup.InstanceName(), Equals, "some-snap")
	c.Assert(snapsupInstance.InstanceName(), Equals, "some-snap_instance")

	verifyUpdateTasks(c, snap.TypeApp, 0, 3, tts[0])
	verifyUpdateTasks(c, snap.TypeApp, 0, 1, tts[1])
}

func (s *snapmgrTestSuite) TestParallelInstanceUpdateManyValidateRefreshes(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	tr := config.NewTransaction(s.state)
	tr.Set("core", "experimental.parallel-instances", true)
	tr.Commit()

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)},
		}),
		Current:  snap.R(1),
		SnapType: "app",
	})
	snapstate.Set(s.state, "some-snap_instance", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)},
		}),
		Current:     snap.R(1),
		SnapType:    "app",
		InstanceKey: "instance",
	})

	validateCalled := false
	validateRefreshes := func(st *state.State, refreshes []*snap.Info, ignoreValidation map[string]bool, userID int, deviceCtx snapstate.DeviceContext) ([]*snap.Info, error) {
		validateCalled = true
		c.Check(refreshes, HasLen, 2)
		instanceIdx := 0
		someIdx := 1
		if refreshes[0].InstanceName() != "some-snap_instance" {
			instanceIdx = 1
			someIdx = 0
		}
		c.Check(refreshes[someIdx].InstanceName(), Equals, "some-snap")
		c.Check(refreshes[instanceIdx].InstanceName(), Equals, "some-snap_instance")
		c.Check(refreshes[0].SnapID, Equals, "some-snap-id")
		c.Check(refreshes[0].Revision, Equals, snap.R(11))
		c.Check(refreshes[1].SnapID, Equals, "some-snap-id")
		c.Check(refreshes[1].Revision, Equals, snap.R(11))
		c.Check(ignoreValidation, HasLen, 0)
		return refreshes, nil
	}
	// hook it up
	snapstate.ValidateRefreshes = validateRefreshes

	updates, tts, err := snapstate.UpdateMany(context.Background(), s.state, nil, nil, 0, nil)
	c.Assert(err, IsNil)
	c.Assert(tts, HasLen, 4)
	verifyReRefreshTasks(c, tts[2])
	verifyDelayedEffectsTasks(c, tts[3], []int{1, 2}, 0)
	sort.Strings(updates)
	c.Check(updates, DeepEquals, []string{"some-snap", "some-snap_instance"})
	verifyUpdateTasks(c, snap.TypeApp, 0, 0, tts[0])
	verifyUpdateTasks(c, snap.TypeApp, 0, 0, tts[1])

	c.Check(validateCalled, Equals, true)
}

func (s *snapmgrTestSuite) TestUpdateManyValidateRefreshesUnhappy(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)},
		}),
		Current: snap.R(1),
	})

	validateErr := errors.New("refresh control error")
	validateRefreshes := func(st *state.State, refreshes []*snap.Info, ignoreValidation map[string]bool, userID int, deviceCtx snapstate.DeviceContext) ([]*snap.Info, error) {
		c.Check(refreshes, HasLen, 1)
		c.Check(refreshes[0].SnapID, Equals, "some-snap-id")
		c.Check(refreshes[0].Revision, Equals, snap.R(11))
		c.Check(ignoreValidation, HasLen, 0)
		return nil, validateErr
	}
	// hook it up
	snapstate.ValidateRefreshes = validateRefreshes

	// refresh all => no error
	updates, tts, err := snapstate.UpdateMany(context.Background(), s.state, nil, nil, 0, nil)
	c.Assert(err, IsNil)
	c.Check(tts, HasLen, 0)
	c.Check(updates, HasLen, 0)

	// refresh some-snap => report error
	updates, tts, err = snapstate.UpdateMany(context.Background(), s.state, []string{"some-snap"}, nil, 0, nil)
	c.Assert(err, Equals, validateErr)
	c.Check(tts, HasLen, 0)
	c.Check(updates, HasLen, 0)

}

func (s *snapmgrTestSuite) testUpdateManyDiskSpaceCheck(c *C, featureFlag, failDiskCheck, failInstallSize bool) error {
	var diskCheckCalled, installSizeCalled bool
	restore := snapstate.MockOsutilCheckFreeSpace(func(path string, sz uint64) error {
		diskCheckCalled = true
		c.Check(path, Equals, filepath.Join(dirs.GlobalRootDir, "/var/lib/snapd"))
		c.Check(sz, Equals, snapstate.SafetyMarginDiskSpace(123))
		if failDiskCheck {
			return &osutil.NotEnoughDiskSpaceError{}
		}
		return nil
	})
	defer restore()

	restoreInstallSize := snapstate.MockInstallSize(func(st *state.State, snaps []snapstate.MinimalInstallInfo, userID int, prqt snapstate.PrereqTracker) (uint64, error) {
		installSizeCalled = true
		if failInstallSize {
			return 0, fmt.Errorf("boom")
		}
		c.Assert(snaps, HasLen, 2)
		c.Check(snaps[0].InstanceName(), Equals, "snapd")
		c.Check(snaps[1].InstanceName(), Equals, "some-snap")
		return 123, nil
	})
	defer restoreInstallSize()

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

	tr := config.NewTransaction(s.state)
	tr.Set("core", "experimental.check-disk-space-refresh", featureFlag)
	tr.Commit()

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)},
		}),
		Current:  snap.R(1),
		SnapType: "app",
	})

	snapstate.Set(s.state, "snapd", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "snapd", SnapID: "snapd-snap-id", Revision: snap.R(1)},
		}),
		Current:  snap.R(1),
		SnapType: "app",
	})

	updates, _, err := snapstate.UpdateMany(context.Background(), s.state, nil, nil, 0, nil)
	if featureFlag {
		c.Check(installSizeCalled, Equals, true)
		if failInstallSize {
			c.Check(diskCheckCalled, Equals, false)
		} else {
			c.Check(diskCheckCalled, Equals, true)
			if failDiskCheck {
				c.Check(updates, HasLen, 0)
			} else {
				c.Check(updates, HasLen, 2)
			}
		}
	} else {
		c.Check(installSizeCalled, Equals, false)
		c.Check(diskCheckCalled, Equals, false)
	}

	return err
}

func (s *snapmgrTestSuite) TestUpdateManyDiskSpaceCheckError(c *C) {
	featureFlag := true
	failDiskCheck := true
	failInstallSize := false
	err := s.testUpdateManyDiskSpaceCheck(c, featureFlag, failDiskCheck, failInstallSize)
	diskSpaceErr := err.(*snapstate.InsufficientSpaceError)
	c.Assert(diskSpaceErr, ErrorMatches, `insufficient space in .* to perform "refresh" change for the following snaps: snapd, some-snap`)
	c.Check(diskSpaceErr.Path, Equals, filepath.Join(dirs.GlobalRootDir, "/var/lib/snapd"))
	c.Check(diskSpaceErr.Snaps, DeepEquals, []string{"snapd", "some-snap"})
}

func (s *snapmgrTestSuite) TestUpdateManyDiskSpaceSkippedIfFeatureDisabled(c *C) {
	featureFlag := false
	failDiskCheck := true
	failInstallSize := false
	err := s.testUpdateManyDiskSpaceCheck(c, featureFlag, failDiskCheck, failInstallSize)
	c.Assert(err, IsNil)
}

func (s *snapmgrTestSuite) TestUpdateManyDiskSpaceFailInstallSize(c *C) {
	featureFlag := true
	failDiskCheck := false
	failInstallSize := true
	err := s.testUpdateManyDiskSpaceCheck(c, featureFlag, failDiskCheck, failInstallSize)
	c.Assert(err, ErrorMatches, "boom")
}

func (s *snapmgrTestSuite) TestUnlinkCurrentSnapLastActiveDisabledServicesSet(c *C) {
	si := snap.SideInfo{
		RealName: "services-snap",
		Revision: snap.R(-42),
	}
	snaptest.MockSnap(c, `name: services-snap`, &si)

	prevCurrentlyDisabled := s.fakeBackend.servicesCurrentlyDisabled
	prevUserCurrentDisabled := s.fakeBackend.userServicesCurrentlyDisabled

	s.fakeBackend.servicesCurrentlyDisabled = []string{"svc1", "svc2"}
	s.fakeBackend.userServicesCurrentlyDisabled = map[int][]string{
		1000: {"svc3"},
	}

	// reset the services to what they were before after the test is done
	defer func() {
		s.fakeBackend.servicesCurrentlyDisabled = prevCurrentlyDisabled
		s.fakeBackend.userServicesCurrentlyDisabled = prevUserCurrentDisabled
	}()

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

	snapstate.Set(s.state, "services-snap", &snapstate.SnapState{
		Active:                         true,
		Sequence:                       snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si}),
		Current:                        si.Revision,
		SnapType:                       "app",
		TrackingChannel:                "stable",
		LastActiveDisabledServices:     []string{},
		LastActiveDisabledUserServices: map[int][]string{},
	})

	chg := s.state.NewChange("refresh", "refresh a snap")
	ts, err := snapstate.Update(s.state, "services-snap", &snapstate.RevisionOptions{Channel: "some-channel"}, s.user.ID, snapstate.Flags{Amend: true})

	c.Assert(err, IsNil)
	// only add up to unlink-current-snap task
	for _, t := range ts.Tasks() {
		chg.AddTask(t)
		if t.Kind() == "unlink-current-snap" {
			// don't add any more from this point on
			break
		}
	}

	s.settle(c)

	c.Assert(chg.Err(), IsNil)
	c.Assert(chg.IsReady(), Equals, true)

	// get the snap state
	var snapst snapstate.SnapState
	err = snapstate.Get(s.state, "services-snap", &snapst)
	c.Assert(err, IsNil)

	// make sure that the disabled services in this snap's state is what we
	// provided
	sort.Strings(snapst.LastActiveDisabledServices)
	c.Assert(snapst.LastActiveDisabledServices, DeepEquals, []string{"svc1", "svc2"})
	c.Assert(snapst.LastActiveDisabledUserServices, testutil.DeepUnsortedMatches, map[int][]string{
		1000: {"svc3"},
	})
}

func (s *snapmgrTestSuite) TestUnlinkCurrentSnapMergedLastActiveDisabledServicesSet(c *C) {
	si := snap.SideInfo{
		RealName: "services-snap",
		Revision: snap.R(-42),
	}
	snaptest.MockSnap(c, `name: services-snap`, &si)

	prevCurrentlyDisabled := s.fakeBackend.servicesCurrentlyDisabled
	prevUserCurrentDisabled := s.fakeBackend.userServicesCurrentlyDisabled

	s.fakeBackend.servicesCurrentlyDisabled = []string{"svc1", "svc2"}
	s.fakeBackend.userServicesCurrentlyDisabled = map[int][]string{
		1000: {"svc3"},
	}

	// reset the services to what they were before after the test is done
	defer func() {
		s.fakeBackend.servicesCurrentlyDisabled = prevCurrentlyDisabled
		s.fakeBackend.userServicesCurrentlyDisabled = prevUserCurrentDisabled
	}()

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

	snapstate.Set(s.state, "services-snap", &snapstate.SnapState{
		Active:                     true,
		Sequence:                   snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si}),
		Current:                    si.Revision,
		SnapType:                   "app",
		TrackingChannel:            "stable",
		LastActiveDisabledServices: []string{"missing-svc3"},
		LastActiveDisabledUserServices: map[int][]string{
			1000: {"svc4-missing"},
		},
	})

	chg := s.state.NewChange("refresh", "refresh a snap")
	ts, err := snapstate.Update(s.state, "services-snap", &snapstate.RevisionOptions{Channel: "some-channel"}, s.user.ID, snapstate.Flags{Amend: true})

	c.Assert(err, IsNil)
	// only add up to unlink-current-snap task
	for _, t := range ts.Tasks() {
		chg.AddTask(t)
		if t.Kind() == "unlink-current-snap" {
			// don't add any more from this point on
			break
		}
	}

	s.settle(c)

	c.Assert(chg.Err(), IsNil)
	c.Assert(chg.IsReady(), Equals, true)

	// get the snap state
	var snapst snapstate.SnapState
	err = snapstate.Get(s.state, "services-snap", &snapst)
	c.Assert(err, IsNil)

	// make sure that the disabled services in this snap's state is what we
	// provided
	sort.Strings(snapst.LastActiveDisabledServices)
	c.Assert(snapst.LastActiveDisabledServices, DeepEquals, []string{"missing-svc3", "svc1", "svc2"})
	c.Assert(snapst.LastActiveDisabledUserServices, testutil.DeepUnsortedMatches, map[int][]string{
		1000: {"svc4-missing", "svc3"},
	})
}

func (s *snapmgrTestSuite) TestUnlinkCurrentSnapPassthroughLastActiveDisabledServicesSet(c *C) {
	si := snap.SideInfo{
		RealName: "services-snap",
		Revision: snap.R(-42),
	}
	snaptest.MockSnap(c, `name: services-snap`, &si)

	prevCurrentlyDisabled := s.fakeBackend.servicesCurrentlyDisabled
	prevUserCurrentDisabled := s.fakeBackend.userServicesCurrentlyDisabled

	s.fakeBackend.servicesCurrentlyDisabled = []string{}
	s.fakeBackend.userServicesCurrentlyDisabled = map[int][]string{}

	// reset the services to what they were before after the test is done
	defer func() {
		s.fakeBackend.servicesCurrentlyDisabled = prevCurrentlyDisabled
		s.fakeBackend.userServicesCurrentlyDisabled = prevUserCurrentDisabled
	}()

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

	snapstate.Set(s.state, "services-snap", &snapstate.SnapState{
		Active:                     true,
		Sequence:                   snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si}),
		Current:                    si.Revision,
		SnapType:                   "app",
		TrackingChannel:            "stable",
		LastActiveDisabledServices: []string{"missing-svc3"},
		LastActiveDisabledUserServices: map[int][]string{
			1000: {"svc4-missing"},
		},
	})

	chg := s.state.NewChange("refresh", "refresh a snap")
	ts, err := snapstate.Update(s.state, "services-snap", &snapstate.RevisionOptions{Channel: "some-channel"}, s.user.ID, snapstate.Flags{Amend: true})

	c.Assert(err, IsNil)
	// only add up to unlink-current-snap task
	for _, t := range ts.Tasks() {
		chg.AddTask(t)
		if t.Kind() == "unlink-current-snap" {
			// don't add any more from this point on
			break
		}
	}

	s.settle(c)

	c.Assert(chg.Err(), IsNil)
	c.Assert(chg.IsReady(), Equals, true)

	// get the snap state
	var snapst snapstate.SnapState
	err = snapstate.Get(s.state, "services-snap", &snapst)
	c.Assert(err, IsNil)

	// make sure that the disabled services in this snap's state is what we
	// provided
	sort.Strings(snapst.LastActiveDisabledServices)
	c.Assert(snapst.LastActiveDisabledServices, DeepEquals, []string{"missing-svc3"})
	c.Assert(snapst.LastActiveDisabledUserServices, testutil.DeepUnsortedMatches, map[int][]string{
		1000: {"svc4-missing"},
	})
}

func (s *snapmgrTestSuite) TestStopSnapServicesSavesSnapSetupLastActiveDisabledServices(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	prevCurrentlyDisabled := s.fakeBackend.servicesCurrentlyDisabled
	prevUserCurrentDisabled := s.fakeBackend.userServicesCurrentlyDisabled

	s.fakeBackend.servicesCurrentlyDisabled = []string{"svc1", "svc2"}
	s.fakeBackend.userServicesCurrentlyDisabled = map[int][]string{
		1000: {"svc3"},
	}

	// reset the services to what they were before after the test is done
	defer func() {
		s.fakeBackend.servicesCurrentlyDisabled = prevCurrentlyDisabled
		s.fakeBackend.userServicesCurrentlyDisabled = prevUserCurrentDisabled
	}()

	snapstate.Set(s.state, "services-snap", &snapstate.SnapState{
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "services-snap", Revision: snap.R(11)},
		}),
		Current: snap.R(11),
		Active:  true,
	})

	snapsup := &snapstate.SnapSetup{
		SideInfo: &snap.SideInfo{
			RealName: "services-snap",
			Revision: snap.R(11),
			SnapID:   "services-snap-id",
		},
	}

	chg := s.state.NewChange("stop-services", "stop the services")
	t1 := s.state.NewTask("prerequisites", "...")
	t1.Set("snap-setup", snapsup)
	t2 := s.state.NewTask("stop-snap-services", "...")
	t2.Set("stop-reason", snap.StopReasonDisable)
	t2.Set("snap-setup-task", t1.ID())
	t2.WaitFor(t1)
	chg.AddTask(t1)
	chg.AddTask(t2)

	s.settle(c)

	c.Assert(chg.Err(), IsNil)
	c.Assert(chg.IsReady(), Equals, true)

	// get the snap state
	var snapst snapstate.SnapState
	c.Assert(snapstate.Get(s.state, "services-snap", &snapst), IsNil)

	// make sure that the disabled services in this snap's state is what we
	// provided
	sort.Strings(snapst.LastActiveDisabledServices)
	c.Assert(snapst.LastActiveDisabledServices, DeepEquals, []string{"svc1", "svc2"})
	c.Assert(snapst.LastActiveDisabledUserServices, testutil.DeepUnsortedMatches, map[int][]string{
		1000: {"svc3"},
	})
}

func (s *snapmgrTestSuite) TestStartSnapServicesCorrectlyKeepsDisabledByHooks(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	snapstate.Set(s.state, "services-snap", &snapstate.SnapState{
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "services-snap", Revision: snap.R(11)},
		}),
		Current: snap.R(11),
		Active:  true,
		// fake this having services disabled by hooks
		ServicesDisabledByHooks: []string{"svc1"},
		UserServicesDisabledByHooks: map[int][]string{
			1000: {"svc3"},
		},
	})

	snapsup := &snapstate.SnapSetup{
		SideInfo: &snap.SideInfo{
			RealName: "services-snap",
			Revision: snap.R(11),
			SnapID:   "services-snap-id",
		},
	}

	chg := s.state.NewChange("start-services", "start the services")
	t1 := s.state.NewTask("prerequisites", "...")
	t1.Set("snap-setup", snapsup)
	t2 := s.state.NewTask("start-snap-services", "...")
	t2.Set("stop-reason", snap.StopReasonDisable)
	t2.Set("snap-setup-task", t1.ID())
	t2.WaitFor(t1)
	chg.AddTask(t1)
	chg.AddTask(t2)

	s.settle(c)

	c.Assert(chg.Err(), IsNil)
	c.Assert(chg.IsReady(), Equals, true)

	// verify correct call was made
	c.Check(s.fakeBackend.ops, DeepEquals, fakeOps{
		fakeOp{
			op:                   "start-snap-services",
			path:                 filepath.Join(dirs.SnapMountDir, "services-snap/11"),
			services:             []string{"svc1", "svc3", "svc2"},
			disabledServices:     []string{"svc1"},
			disabledUserServices: map[int][]string{1000: {"svc3"}},
		},
	})
}

func (s *snapmgrTestSuite) TestStopSnapServicesFirstSavesSnapSetupLastActiveDisabledServices(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	prevCurrentlyDisabled := s.fakeBackend.servicesCurrentlyDisabled
	prevUserCurrentDisabled := s.fakeBackend.userServicesCurrentlyDisabled

	s.fakeBackend.servicesCurrentlyDisabled = []string{"svc1"}
	s.fakeBackend.userServicesCurrentlyDisabled = map[int][]string{
		1000: {"svc3"},
	}

	// reset the services to what they were before after the test is done
	defer func() {
		s.fakeBackend.servicesCurrentlyDisabled = prevCurrentlyDisabled
		s.fakeBackend.userServicesCurrentlyDisabled = prevUserCurrentDisabled
	}()

	snapstate.Set(s.state, "services-snap", &snapstate.SnapState{
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "services-snap", Revision: snap.R(11)},
		}),
		Current: snap.R(11),
		Active:  true,
		// leave this line to keep gofmt 1.10 happy
		LastActiveDisabledServices: []string{"svc2"},
		LastActiveDisabledUserServices: map[int][]string{
			1000: {"svc4"},
		},
	})

	snapsup := &snapstate.SnapSetup{
		SideInfo: &snap.SideInfo{
			RealName: "services-snap",
			Revision: snap.R(11),
			SnapID:   "services-snap-id",
		},
	}

	chg := s.state.NewChange("stop-services", "stop the services")
	t := s.state.NewTask("stop-snap-services", "...")
	t.Set("stop-reason", snap.StopReasonDisable)
	t.Set("snap-setup", snapsup)
	chg.AddTask(t)

	s.settle(c)

	c.Assert(chg.Err(), IsNil)
	c.Assert(chg.IsReady(), Equals, true)

	// get the snap state
	var snapst snapstate.SnapState
	c.Assert(snapstate.Get(s.state, "services-snap", &snapst), IsNil)

	// make sure that the disabled services in this snap's state is what we
	// provided
	sort.Strings(snapst.LastActiveDisabledServices)
	c.Assert(snapst.LastActiveDisabledServices, DeepEquals, []string{"svc1", "svc2"})
	c.Assert(snapst.LastActiveDisabledUserServices, testutil.DeepUnsortedMatches, map[int][]string{
		1000: {"svc4", "svc3"},
	})
}

func (s *snapmgrTestSuite) TestRefreshDoesntRestoreRevisionConfig(c *C) {
	restore := release.MockOnClassic(false)
	defer restore()

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

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)},
		}),
		Current:  snap.R(1),
		SnapType: "app",
	})

	// set global configuration (affecting current snap)
	tr := config.NewTransaction(s.state)
	tr.Set("some-snap", "foo", "100")
	tr.Commit()

	// set per-revision config for the upcoming rev. 2, we don't expect it restored though
	// since only revert restores revision configs.
	s.state.Set("revision-config", map[string]any{
		"some-snap": map[string]any{
			"2": map[string]any{"foo": "200"},
		},
	})

	// simulate a refresh to rev. 2
	chg := s.state.NewChange("update", "update some-snap")
	ts, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "some-channel", Revision: snap.R(2)}, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)
	chg.AddAll(ts)

	s.settle(c)

	// config of rev. 1 has been stored in per-revision map
	var cfgs map[string]any
	c.Assert(s.state.Get("revision-config", &cfgs), IsNil)
	c.Assert(cfgs["some-snap"], DeepEquals, map[string]any{
		"1": map[string]any{"foo": "100"},
		"2": map[string]any{"foo": "200"},
	})

	// config of rev. 2 hasn't been restored by refresh, old value returned
	tr = config.NewTransaction(s.state)
	var res string
	c.Assert(tr.Get("some-snap", "foo", &res), IsNil)
	c.Assert(res, Equals, "100")
}

func (s *snapmgrTestSuite) TestUpdateContentProviderDownloadFailure(c *C) {
	restore := release.MockOnClassic(true)
	defer restore()

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

	// trigger download error on content provider
	s.fakeStore.downloadError["snap-content-slot"] = fmt.Errorf("boom")

	snapstate.ReplaceStore(s.state, contentStore{fakeStore: s.fakeStore, state: s.state})

	si := &snap.SideInfo{
		RealName: "snap-content-plug",
		SnapID:   "snap-content-plug-id",
		Revision: snap.R(7),
	}
	snaptest.MockSnap(c, `name: snap-content-plug`, si)
	snapstate.Set(s.state, "snap-content-plug", &snapstate.SnapState{
		Active:          true,
		TrackingChannel: "latest/edge",
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
		Current:         snap.R(7),
		SnapType:        "app",
	})
	snapstate.Set(s.state, "snap-content-slot", &snapstate.SnapState{
		Active:          true,
		TrackingChannel: "latest/stable",
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{
			RealName: "snap-content-slot",
			SnapID:   "snap-content-slot-id",
			Revision: snap.R(1),
		}}),
		Current:  snap.R(1),
		SnapType: "app",
	})

	chg := s.state.NewChange("refresh", "refresh all snaps")
	updated, tts, err := snapstate.UpdateMany(context.Background(), s.state, nil, nil, 0, nil)
	c.Assert(err, IsNil)
	c.Check(updated, testutil.DeepUnsortedMatches, []string{"snap-content-plug", "snap-content-slot"})
	for _, ts := range tts {
		chg.AddAll(ts)
	}

	s.settle(c)

	// content consumer snap fails to download
	c.Assert(chg.Err(), ErrorMatches, "cannot perform the following tasks:\n.*Download snap \"snap-content-slot\" \\(11\\) from channel \"latest/stable\" \\(boom\\).*")
	c.Assert(chg.IsReady(), Equals, true)

	var snapSt snapstate.SnapState
	// content provider not updated due to download failure
	c.Assert(snapstate.Get(s.state, "snap-content-slot", &snapSt), IsNil)
	c.Check(snapSt.Current, Equals, snap.R(1))

	c.Assert(snapstate.Get(s.state, "snap-content-plug", &snapSt), IsNil)
	// but content consumer got updated to the new revision
	c.Check(snapSt.Current, Equals, snap.R(11))
}

func (s *snapmgrTestSuite) TestNoReRefreshInUpdate(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)},
		}),
		Current:  snap.R(1),
		SnapType: "app",
	})

	ts, err := snapstate.Update(s.state, "some-snap", nil, 0, snapstate.Flags{NoReRefresh: true})
	c.Assert(err, IsNil)

	// ensure we have no re-refresh task
	for _, t := range ts.Tasks() {
		c.Assert(t.Kind(), Not(Equals), "check-rerefresh")
	}

	snapsup, err := snapstate.TaskSnapSetup(ts.Tasks()[0])
	c.Assert(err, IsNil)
	// NoReRefresh is consumed and consulted when creating the taskset
	// but is not copied into SnapSetup
	c.Check(snapsup.Flags.NoReRefresh, Equals, false)
}

func (s *snapmgrTestSuite) TestEmptyUpdateWithChannelChangeAndAutoAlias(c *C) {
	// this reproduces the cause behind lp:1860324,
	// namely an empty refresh with a channel change on a snap
	// with changed aliases

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

	n := 0
	snapstate.AutoAliases = func(st *state.State, info *snap.Info) (map[string]string, error) {
		if info.InstanceName() == "alias-snap" {
			if n > 0 {
				return map[string]string{
					"alias1": "cmd1",
					"alias2": "cmd2",
				}, nil
			}
			n++
		}
		return nil, nil
	}

	snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{
		TrackingChannel: "latest/stable",
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "alias-snap", Revision: snap.R(11), SnapID: "alias-snap-id"},
		}),
		Current: snap.R(11),
		Active:  true,
	})

	s.state.Set("aliases", map[string]map[string]string{
		"alias-snap": {
			"alias1": "auto",
		},
	})

	s.state.Unlock()
	err := s.snapmgr.Ensure()
	s.state.Lock()
	c.Assert(err, IsNil)

	ts, err := snapstate.Update(s.state, "alias-snap", &snapstate.RevisionOptions{Channel: "latest/candidate"}, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)

	chg := s.state.NewChange("refresh", "refresh snap")
	chg.AddAll(ts)

	s.settle(c)

	c.Assert(chg.Err(), IsNil)
	c.Assert(chg.IsReady(), Equals, true)
}

func (s *snapmgrTestSuite) testUpdateDiskSpaceCheck(c *C, featureFlag, failInstallSize, failDiskCheck bool) error {
	restore := snapstate.MockOsutilCheckFreeSpace(func(path string, sz uint64) error {
		c.Check(sz, Equals, snapstate.SafetyMarginDiskSpace(123))
		if failDiskCheck {
			return &osutil.NotEnoughDiskSpaceError{}
		}
		return nil
	})
	defer restore()

	var installSizeCalled bool

	restoreInstallSize := snapstate.MockInstallSize(func(st *state.State, snaps []snapstate.MinimalInstallInfo, userID int, prqt snapstate.PrereqTracker) (uint64, error) {
		installSizeCalled = true
		if failInstallSize {
			return 0, fmt.Errorf("boom")
		}
		c.Assert(snaps, HasLen, 1)
		c.Check(snaps[0].InstanceName(), Equals, "some-snap")
		return 123, nil
	})
	defer restoreInstallSize()

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

	tr := config.NewTransaction(s.state)
	tr.Set("core", "experimental.check-disk-space-refresh", featureFlag)
	tr.Commit()

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(4)},
		}),
		Current:  snap.R(4),
		SnapType: "app",
	})

	opts := &snapstate.RevisionOptions{Channel: "some-channel"}
	_, err := snapstate.Update(s.state, "some-snap", opts, s.user.ID, snapstate.Flags{})

	if featureFlag {
		c.Check(installSizeCalled, Equals, true)
	} else {
		c.Check(installSizeCalled, Equals, false)
	}

	return err
}

func (s *snapmgrTestSuite) TestUpdateDiskSpaceError(c *C) {
	featureFlag := true
	failInstallSize := false
	failDiskCheck := true
	err := s.testUpdateDiskSpaceCheck(c, featureFlag, failInstallSize, failDiskCheck)
	diskSpaceErr := err.(*snapstate.InsufficientSpaceError)
	c.Assert(diskSpaceErr, ErrorMatches, `insufficient space in .* to perform "refresh" change for the following snaps: some-snap`)
	c.Check(diskSpaceErr.Path, Equals, filepath.Join(dirs.GlobalRootDir, "/var/lib/snapd"))
	c.Check(diskSpaceErr.Snaps, DeepEquals, []string{"some-snap"})
}

func (s *snapmgrTestSuite) TestUpdateDiskCheckSkippedIfDisabled(c *C) {
	featureFlag := false
	failInstallSize := false
	failDiskCheck := true
	err := s.testUpdateDiskSpaceCheck(c, featureFlag, failInstallSize, failDiskCheck)
	c.Check(err, IsNil)
}

func (s *snapmgrTestSuite) TestUpdateDiskCheckInstallSizeError(c *C) {
	featureFlag := true
	failInstallSize := true
	failDiskCheck := false
	err := s.testUpdateDiskSpaceCheck(c, featureFlag, failInstallSize, failDiskCheck)
	c.Check(err, ErrorMatches, "boom")
}

func (s *snapmgrTestSuite) TestUpdateDiskCheckHappy(c *C) {
	featureFlag := true
	failInstallSize := false
	failDiskCheck := false
	err := s.testUpdateDiskSpaceCheck(c, featureFlag, failInstallSize, failDiskCheck)
	c.Check(err, IsNil)
}

func (s *snapmgrTestSuite) TestUpdateSnapAndOutdatedPrereq(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	updateSnaps := []string{"outdated-consumer", "outdated-producer"}
	for _, snapName := range updateSnaps {
		snapstate.Set(s.state, snapName, &snapstate.SnapState{
			Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{
				RealName: snapName,
				SnapID:   fmt.Sprintf("%s-id", snapName),
				Revision: snap.R(1),
			}}),
			Current: snap.R(1),
			Active:  true,
		})
	}

	chg := s.state.NewChange("refresh-snap", "test: update snaps")
	updated, tss, err := snapstate.UpdateMany(context.Background(), s.state, updateSnaps, nil, s.user.ID, nil)
	c.Assert(err, IsNil)
	c.Check(tss, Not(HasLen), 0)
	c.Assert(updated, testutil.DeepUnsortedMatches, updateSnaps)

	for _, ts := range tss {
		chg.AddAll(ts)
	}
	s.settle(c)

	c.Assert(chg.Err(), IsNil)
	c.Assert(chg.Status(), Equals, state.DoneStatus)

	c.Check(s.fakeStore.downloads, testutil.DeepUnsortedMatches, []fakeDownload{
		{macaroon: s.user.StoreMacaroon, name: "outdated-consumer", target: filepath.Join(dirs.SnapBlobDir, "outdated-consumer_11.snap")},
		{macaroon: s.user.StoreMacaroon, name: "outdated-producer", target: filepath.Join(dirs.SnapBlobDir, "outdated-producer_11.snap")},
	})
}

func (s *snapmgrTestSuite) TestUpdatePrereqDetectConflictWithPrereq(c *C) {
	s.state.Lock()

	snapstate.Set(s.state, "outdated-producer", &snapstate.SnapState{
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{
			RealName: "outdated-producer",
			SnapID:   "outdated-producer-id",
			Revision: snap.R(1),
		}}),
		Current: snap.R(1),
		Active:  false,
	})
	snapstate.Set(s.state, "outdated-consumer", &snapstate.SnapState{
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{
			RealName: "outdated-consumer",
			SnapID:   "outdated-consumer-id",
			Revision: snap.R(1),
		}}),
		Current: snap.R(1),
		Active:  true,
	})

	enableTasks, err := snapstate.Enable(s.state, "outdated-producer")
	c.Assert(err, IsNil)
	c.Check(enableTasks, Not(HasLen), 0)
	enableChg := s.state.NewChange("enable-snap", "test: enable snap")
	enableChg.AddAll(enableTasks)

	// this update triggers an update of the producer which conflicts with the
	// 'Enable' op. This should be detected before it tries to update the producer
	updateTasks, err := snapstate.Update(s.state, "outdated-consumer", nil, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)
	c.Check(updateTasks, Not(HasLen), 0)
	updateChg := s.state.NewChange("refresh-snap", "test: update snap")
	updateChg.AddAll(updateTasks)

	s.state.Unlock()
	_ = s.o.Settle(3 * time.Second)

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

	prereqTask, _ := findPrereqTasksForSnap(c, updateChg, "outdated-consumer")

	// check that it's not done and that it was scheduled for a specific time
	// (only done when retrying). This doesn't test that it's scheduled for
	// sometime in the future to avoid race conditions on slower systems
	c.Check(prereqTask.Status(), Equals, state.DoingStatus)
	c.Assert(prereqTask.AtTime().IsZero(), Equals, false)
}

func (s *snapmgrTestSuite) TestUpdatePrereqWithConflictingTask(c *C) {
	s.state.Lock()

	prodInfo := &snap.SideInfo{
		RealName: "outdated-producer",
		SnapID:   "outdated-producer-id",
		Revision: snap.R(1),
	}
	snapstate.Set(s.state, "outdated-producer", &snapstate.SnapState{
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{prodInfo}),
		Current:  snap.R(1),
		Active:   true,
	})
	snapstate.Set(s.state, "outdated-consumer", &snapstate.SnapState{
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{
			RealName: "outdated-consumer",
			SnapID:   "outdated-consumer-id",
			Revision: snap.R(1),
		}}),
		Current: snap.R(1),
		Active:  true,
	})

	// the Update op will conflict with this task and it should be retried
	chg := s.state.NewChange("test", "")
	task := s.state.NewTask("test", "")
	task.SetStatus(state.DoStatus)
	task.Set("snap-setup", &snapstate.SnapSetup{SideInfo: prodInfo})
	chg.AddTask(task)

	// the update of the producer should be scheduled but conflict in the Update call.
	// That should still result in the task being retried
	updateTasks, err := snapstate.Update(s.state, "outdated-consumer", nil, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)
	c.Check(updateTasks, Not(HasLen), 0)
	updateChg := s.state.NewChange("refresh-snap", "test: update snap")
	updateChg.AddAll(updateTasks)

	s.state.Unlock()
	_ = s.o.Settle(3 * time.Second)

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

	prereqTask, _ := findPrereqTasksForSnap(c, updateChg, "outdated-consumer")

	// check that it's not done and that it was scheduled for a specific time
	// (only done when retrying). This doesn't test that it's scheduled for
	// sometime in the future to avoid race conditions on slower systems
	c.Check(prereqTask.Status(), Equals, state.DoingStatus)
	c.Assert(prereqTask.AtTime().IsZero(), Equals, false)
}

func (s *snapmgrTestSuite) TestUpdateNoRetryIfPrereqTaskFails(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	snapstate.Set(s.state, "outdated-producer", &snapstate.SnapState{
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{
			RealName: "outdated-producer",
			SnapID:   "outdated-producer-id",
			Revision: snap.R(1),
		}}),
		Current: snap.R(1),
		// this will cause the update refresh to fail but the (prerequisites) task
		// shouldn't be retried
		Active: false,
	})
	snapstate.Set(s.state, "outdated-consumer", &snapstate.SnapState{
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{
			RealName: "outdated-consumer",
			SnapID:   "outdated-consumer-id",
			Revision: snap.R(1),
		}}),
		Current: snap.R(1),
		Active:  true,
	})

	// the update of the producer should be attempted but fail and not be retried
	updateTasks, err := snapstate.Update(s.state, "outdated-consumer", nil, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)
	c.Check(updateTasks, Not(HasLen), 0)
	updateChg := s.state.NewChange("refresh-snap", "test: update snap")
	updateChg.AddAll(updateTasks)

	s.settle(c)

	prereqTask, _ := findPrereqTasksForSnap(c, updateChg, "outdated-consumer")

	// check that the task is done and that it wasn't ever rescheduled for a
	// specific time (only done when retrying)
	c.Check(prereqTask.Status(), Equals, state.DoneStatus)
	c.Assert(prereqTask.AtTime().IsZero(), Equals, true)
}

func (s *snapmgrTestSuite) TestUpdatePrereqIgnoreDuplOpInSameChange(c *C) {
	s.state.Lock()

	prodInfo := &snap.SideInfo{
		RealName: "outdated-producer",
		SnapID:   "outdated-producer-id",
		Revision: snap.R(1),
	}
	snapstate.Set(s.state, "outdated-producer", &snapstate.SnapState{
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{prodInfo}),
		Current:  snap.R(1),
		Active:   true,
	})
	snapstate.Set(s.state, "outdated-consumer", &snapstate.SnapState{
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{
			RealName: "outdated-consumer",
			SnapID:   "outdated-consumer-id",
			Revision: snap.R(1),
		}}),
		Current: snap.R(1),
		Active:  true,
	})

	chg := s.state.NewChange("refresh-snap", "test: update snap")

	// we inject a conflicting task to simulate a concurrent update
	// (same snap and same change) for determinism. Using UpdateMany
	// would create a race between the update operations
	confTask := s.state.NewTask("conflicting-task", "")
	confTask.Set("snap-setup", &snapstate.SnapSetup{SideInfo: prodInfo})
	chg.AddTask(confTask)

	updateTasks, err := snapstate.Update(s.state, "outdated-consumer", nil, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)
	c.Check(updateTasks.Tasks(), Not(HasLen), 0)
	chg.AddAll(updateTasks)

	s.state.Unlock()
	// the tasks won't converge because the re-refresh waits for all tasks
	// in the change, including our 'conflicting-task'
	_ = s.o.Settle(3 * time.Second)

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

	// check that the prereq task wasn't retried
	prereqTask, _ := findPrereqTasksForSnap(c, chg, "outdated-consumer")
	c.Check(prereqTask.Status(), Equals, state.DoneStatus)
	c.Assert(prereqTask.AtTime().IsZero(), Equals, true)
}

func (s *snapmgrTestSuite) TestUpdateSyncPrereqRetriesForSameChangeBase(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	restore := snapstate.MockPrerequisitesRetryTimeout(time.Millisecond)
	s.AddCleanup(restore)

	snapstate.Set(s.state, "snapd", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{
			RealName: "snapd",
			SnapID:   "snapd-snap-id",
			Revision: snap.R(1),
		}}),
		Current:         snap.R(1),
		TrackingChannel: "latest/stable",
		SnapType:        "snapd",
	})
	snapstate.Set(s.state, "core18", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{
			RealName: "core18",
			SnapID:   "core18-snap-id",
			Revision: snap.R(1),
		}}),
		Current:         snap.R(1),
		TrackingChannel: "latest/stable",
		SnapType:        "base",
	})
	snapstate.Set(s.state, "some-snap-with-core18-base", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{
			RealName: "some-snap-with-core18-base",
			SnapID:   "some-snap-with-core18-base",
			Revision: snap.R(1),
		}}),
		Current:         snap.R(1),
		TrackingChannel: "latest/stable",
		SnapType:        "app",
		Base:            "core18",
	})

	goal := snapstate.StoreUpdateGoal(snapstate.StoreUpdate{
		InstanceName: "some-snap-with-core18-base",
	})
	appTasks, err := snapstate.UpdateOne(context.Background(), s.state, goal, nil, snapstate.Options{
		Flags: snapstate.Flags{
			Transaction: client.TransactionPerSnap,
		},
		UserID: s.user.ID,
	})
	c.Assert(err, IsNil)

	chg := s.state.NewChange("refresh", "refresh some-snap-with-core18-base")
	chg.AddAll(appTasks)

	// this uses a real task graph for the app, but injects in-flight same-change
	// base work so that the generated sync prerequisites task has to poll for
	// the base's link-snap to finish.
	baseLink := s.state.NewTask("link-snap", "pretend core18 gets linked")
	baseLink.Set("snap-setup", &snapstate.SnapSetup{
		SideInfo: &snap.SideInfo{
			RealName: "core18",
			SnapID:   "core18-snap-id",
			Revision: snap.R(11),
		},
		Type: snap.TypeBase,
	})
	baseLink.At(time.Now().Add(time.Hour))
	chg.AddTask(baseLink)

	earlyPrereq, syncPrereq := findPrereqTasksForSnap(c, chg, "some-snap-with-core18-base")

	s.state.Unlock()
	err = s.o.SettleWithBreakCondition(testutil.HostScaledTimeout(10*time.Second), func() bool {
		s.state.Lock()
		defer s.state.Unlock()
		return syncPrereq.Status() == state.DoingStatus && !syncPrereq.AtTime().IsZero()
	})
	s.state.Lock()
	c.Assert(err, IsNil)

	// first prereq task is done
	c.Check(earlyPrereq.Status(), Equals, state.DoneStatus)
	c.Check(earlyPrereq.AtTime().IsZero(), Equals, true)

	// sync prereq task has been attempted and is going to be retried
	c.Check(syncPrereq.Status(), Equals, state.DoingStatus)
	c.Check(syncPrereq.AtTime().IsZero(), Equals, false)
	c.Check(baseLink.Status(), Equals, state.DoStatus)

	baseLink.SetStatus(state.DoneStatus)

	s.settle(c)

	c.Check(syncPrereq.Status(), Equals, state.DoneStatus)
	c.Check(baseLink.Status(), Equals, state.DoneStatus)
	c.Check(chg.Status(), Equals, state.DoneStatus)
}

// findPrereqTasksForSnap looks for the early and sync prerequisites tasks for
// the given snap and fails if either is missing or duplicated.
func findPrereqTasksForSnap(c *C, chg *state.Change, snapName string) (earlyPrereq, syncPrereq *state.Task) {
	checkDuplicate := func(kind string, task, existing *state.Task) {
		if existing != nil {
			c.Fatalf("encountered two %s prerequisites tasks for %q in the change but only expected one: \n%s\n%s\n",
				kind, snapName, task.Summary(), existing.Summary())
		}
	}

	for _, task := range chg.Tasks() {
		if task.Kind() != "prerequisites" {
			continue
		}

		snapsup, err := snapstate.TaskSnapSetup(task)
		c.Assert(err, IsNil)
		if snapsup.InstanceName() != snapName {
			continue
		}

		if task.Has("prerequisites-sync") {
			checkDuplicate("sync", task, syncPrereq)
			syncPrereq = task
		} else {
			checkDuplicate("early", task, earlyPrereq)
			earlyPrereq = task
		}
	}

	c.Assert(earlyPrereq, NotNil)
	c.Assert(syncPrereq, NotNil)
	return earlyPrereq, syncPrereq
}

func (s *validationSetsSuite) TestUpdateSnapRequiredByValidationSetAlreadyAtRequiredRevision(c *C) {
	restore := snapstate.MockEnforcedValidationSets(func(st *state.State, extraVss ...*asserts.ValidationSet) (*snapasserts.ValidationSets, error) {
		vs := snapasserts.NewValidationSets()
		someSnap := map[string]any{
			"id":       "yOqKhntON3vR7kwEbVPsILm7bUViPDzx",
			"name":     "some-snap",
			"presence": "required",
			"revision": "4",
		}
		vsa1 := s.mockValidationSetAssert(c, "bar", "1", someSnap)
		vs.Add(vsa1.(*asserts.ValidationSet))
		return vs, nil
	})
	defer restore()

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

	tr := assertstate.ValidationSetTracking{
		AccountID: "foo",
		Name:      "bar",
		Mode:      assertstate.Enforce,
		Current:   1,
	}
	assertstate.UpdateValidationSet(s.state, &tr)

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(4)},
		}),
		Current:  snap.R(4),
		SnapType: "app",
	})

	_, err := snapstate.Update(s.state, "some-snap", nil, 0, snapstate.Flags{})
	c.Assert(err, ErrorMatches, `snap has no updates available`)
}

func (s *validationSetsSuite) TestUpdateSnapRequiredByValidationRefreshToRequiredRevision(c *C) {
	restore := snapstate.MockEnforcedValidationSets(func(st *state.State, extraVss ...*asserts.ValidationSet) (*snapasserts.ValidationSets, error) {
		vs := snapasserts.NewValidationSets()
		someSnap := map[string]any{
			"id":       "yOqKhntON3vR7kwEbVPsILm7bUViPDzx",
			"name":     "some-snap",
			"presence": "required",
			"revision": "11",
		}
		vsa1 := s.mockValidationSetAssert(c, "bar", "1", someSnap)
		vs.Add(vsa1.(*asserts.ValidationSet))
		return vs, nil
	})
	defer restore()

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

	tr := assertstate.ValidationSetTracking{
		AccountID: "foo",
		Name:      "bar",
		Mode:      assertstate.Enforce,
		Current:   1,
	}
	assertstate.UpdateValidationSet(s.state, &tr)

	si := &snap.SideInfo{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)}
	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
		Current:  snap.R(1),
		SnapType: "app",
	})
	snaptest.MockSnap(c, `name: some-snap`, si)

	refreshedDate := fakeRevDateEpoch.AddDate(0, 0, 1)

	ts, err := snapstate.Update(s.state, "some-snap", nil, 0, snapstate.Flags{})
	c.Assert(err, IsNil)

	var snapsup snapstate.SnapSetup
	err = ts.Tasks()[0].Get("snap-setup", &snapsup)
	c.Assert(err, IsNil)
	// new snap revision from the store
	c.Check(snapsup.Revision(), Equals, snap.R(11))

	c.Assert(s.fakeBackend.ops, HasLen, 2)
	expectedOps := fakeOps{{
		op: "storesvc-snap-action",
		curSnaps: []store.CurrentSnap{{
			InstanceName:  "some-snap",
			SnapID:        "some-snap-id",
			Revision:      snap.R(1),
			Epoch:         snap.E("1*"),
			RefreshedDate: refreshedDate,
			Resources:     make(map[string]snap.Revision),
		}}}, {
		op: "storesvc-snap-action:action",
		action: store.SnapAction{
			Action:         "refresh",
			InstanceName:   "some-snap",
			SnapID:         "some-snap-id",
			Revision:       snap.R(11),
			ValidationSets: []snapasserts.ValidationSetKey{"16/foo/bar/1"},
			Flags:          store.SnapActionEnforceValidation,
		},
		revno: snap.R(11),
	}}
	c.Assert(s.fakeBackend.ops, DeepEquals, expectedOps)
}

func (s *validationSetsSuite) TestUpdateSnapRequiredByValidationSetAnyRevision(c *C) {
	restore := snapstate.MockEnforcedValidationSets(func(st *state.State, extraVss ...*asserts.ValidationSet) (*snapasserts.ValidationSets, error) {
		vs := snapasserts.NewValidationSets()
		// no revision specified
		someSnap := map[string]any{
			"id":       "yOqKhntON3vR7kwEbVPsILm7bUViPDzx",
			"name":     "some-snap",
			"presence": "required",
		}
		vsa1 := s.mockValidationSetAssert(c, "bar", "2", someSnap)
		vs.Add(vsa1.(*asserts.ValidationSet))
		return vs, nil
	})
	defer restore()

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

	tr := assertstate.ValidationSetTracking{
		AccountID: "foo",
		Name:      "bar",
		Mode:      assertstate.Enforce,
		Current:   2,
	}
	assertstate.UpdateValidationSet(s.state, &tr)

	si := &snap.SideInfo{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)}
	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
		Current:  snap.R(1),
		SnapType: "app",
	})
	snaptest.MockSnap(c, `name: some-snap`, si)

	refreshedDate := fakeRevDateEpoch.AddDate(0, 0, 1)

	ts, err := snapstate.Update(s.state, "some-snap", nil, 0, snapstate.Flags{})
	c.Assert(err, IsNil)

	var snapsup snapstate.SnapSetup
	err = ts.Tasks()[0].Get("snap-setup", &snapsup)
	c.Assert(err, IsNil)
	// new snap revision from the store
	c.Check(snapsup.Revision(), Equals, snap.R(11))

	c.Assert(s.fakeBackend.ops, HasLen, 2)
	expectedOps := fakeOps{{
		op: "storesvc-snap-action",
		curSnaps: []store.CurrentSnap{{
			InstanceName:  "some-snap",
			SnapID:        "some-snap-id",
			Revision:      snap.R(1),
			Epoch:         snap.E("1*"),
			RefreshedDate: refreshedDate,
			Resources:     make(map[string]snap.Revision),
		}}}, {
		op: "storesvc-snap-action:action",
		action: store.SnapAction{
			Action:         "refresh",
			InstanceName:   "some-snap",
			SnapID:         "some-snap-id",
			ValidationSets: []snapasserts.ValidationSetKey{"16/foo/bar/2"},
			Flags:          store.SnapActionEnforceValidation,
		},
		revno: snap.R(11),
	}}
	c.Assert(s.fakeBackend.ops, DeepEquals, expectedOps)
}

func (s *validationSetsSuite) TestUpdateToRevisionSnapRequiredByValidationSetAnyRevision(c *C) {
	restore := snapstate.MockEnforcedValidationSets(func(st *state.State, extraVss ...*asserts.ValidationSet) (*snapasserts.ValidationSets, error) {
		vs := snapasserts.NewValidationSets()
		// no revision specified
		someSnap := map[string]any{
			"id":       "yOqKhntON3vR7kwEbVPsILm7bUViPDzx",
			"name":     "some-snap",
			"presence": "required",
		}
		vsa1 := s.mockValidationSetAssert(c, "bar", "2", someSnap)
		vs.Add(vsa1.(*asserts.ValidationSet))
		return vs, nil
	})
	defer restore()

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

	tr := assertstate.ValidationSetTracking{
		AccountID: "foo",
		Name:      "bar",
		Mode:      assertstate.Enforce,
		Current:   2,
	}
	assertstate.UpdateValidationSet(s.state, &tr)

	si := &snap.SideInfo{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)}
	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
		Current:  snap.R(1),
		SnapType: "app",
	})
	snaptest.MockSnap(c, `name: some-snap`, si)

	refreshedDate := fakeRevDateEpoch.AddDate(0, 0, 1)

	ts, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Revision: snap.R(11)}, 0, snapstate.Flags{})
	c.Assert(err, IsNil)

	var snapsup snapstate.SnapSetup
	err = ts.Tasks()[0].Get("snap-setup", &snapsup)
	c.Assert(err, IsNil)

	// new snap revision from the store
	c.Check(snapsup.Revision(), Equals, snap.R(11))

	c.Assert(s.fakeBackend.ops, HasLen, 2)
	expectedOps := fakeOps{{
		op: "storesvc-snap-action",
		curSnaps: []store.CurrentSnap{{
			InstanceName:  "some-snap",
			SnapID:        "some-snap-id",
			Revision:      snap.R(1),
			Epoch:         snap.E("1*"),
			RefreshedDate: refreshedDate,
			Resources:     make(map[string]snap.Revision),
		}}}, {
		op: "storesvc-snap-action:action",
		action: store.SnapAction{
			Action:         "refresh",
			InstanceName:   "some-snap",
			SnapID:         "some-snap-id",
			Revision:       snap.R(11),
			ValidationSets: []snapasserts.ValidationSetKey{"16/foo/bar/2"},
		},
		revno: snap.R(11),
	}}
	c.Assert(s.fakeBackend.ops, DeepEquals, expectedOps)
}

func (s *validationSetsSuite) TestUpdateToRevisionSnapRequiredByValidationWithMatchingRevision(c *C) {
	restore := snapstate.MockEnforcedValidationSets(func(st *state.State, extraVss ...*asserts.ValidationSet) (*snapasserts.ValidationSets, error) {
		vs := snapasserts.NewValidationSets()
		someSnap := map[string]any{
			"id":       "yOqKhntON3vR7kwEbVPsILm7bUViPDzx",
			"name":     "some-snap",
			"presence": "required",
			"revision": "11",
		}
		vsa1 := s.mockValidationSetAssert(c, "bar", "2", someSnap)
		vs.Add(vsa1.(*asserts.ValidationSet))
		return vs, nil
	})
	defer restore()

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

	tr := assertstate.ValidationSetTracking{
		AccountID: "foo",
		Name:      "bar",
		Mode:      assertstate.Enforce,
		Current:   2,
	}
	assertstate.UpdateValidationSet(s.state, &tr)

	si := &snap.SideInfo{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)}
	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
		Current:  snap.R(1),
		SnapType: "app",
	})
	snaptest.MockSnap(c, `name: some-snap`, si)

	ts, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Revision: snap.R(11)}, 0, snapstate.Flags{})
	c.Assert(err, IsNil)

	var snapsup snapstate.SnapSetup
	err = ts.Tasks()[0].Get("snap-setup", &snapsup)
	c.Assert(err, IsNil)
	// new snap revision from the store
	c.Check(snapsup.Revision(), Equals, snap.R(11))
	c.Assert(s.fakeBackend.ops, HasLen, 2)
	expectedOps := fakeOps{{
		op: "storesvc-snap-action",
		curSnaps: []store.CurrentSnap{{
			InstanceName:  "some-snap",
			SnapID:        "some-snap-id",
			Revision:      snap.R(1),
			Epoch:         snap.E("1*"),
			RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 1),
			Resources:     make(map[string]snap.Revision),
		}},
	}, {
		op: "storesvc-snap-action:action",
		action: store.SnapAction{
			Action:         "refresh",
			InstanceName:   "some-snap",
			SnapID:         "some-snap-id",
			Revision:       snap.R(11),
			ValidationSets: []snapasserts.ValidationSetKey{"16/foo/bar/2"},
			// XXX: updateToRevisionInfo doesn't set store.SnapActionEnforceValidation flag?
		},
		revno: snap.R(11),
	}}
	c.Assert(s.fakeBackend.ops, DeepEquals, expectedOps)
}

func (s *validationSetsSuite) TestUpdateToRevisionSnapRequiredByValidationAlreadyAtRevisionNoop(c *C) {
	restore := snapstate.MockEnforcedValidationSets(func(st *state.State, extraVss ...*asserts.ValidationSet) (*snapasserts.ValidationSets, error) {
		vs := snapasserts.NewValidationSets()
		someSnap := map[string]any{
			"id":       "yOqKhntON3vR7kwEbVPsILm7bUViPDzx",
			"name":     "some-snap",
			"presence": "required",
			"revision": "4",
		}
		vsa1 := s.mockValidationSetAssert(c, "bar", "2", someSnap)
		vs.Add(vsa1.(*asserts.ValidationSet))
		return vs, nil
	})
	defer restore()

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

	tr := assertstate.ValidationSetTracking{
		AccountID: "foo",
		Name:      "bar",
		Mode:      assertstate.Enforce,
		Current:   2,
	}
	assertstate.UpdateValidationSet(s.state, &tr)

	// revision 4 is already installed
	si := &snap.SideInfo{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(4)}
	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
		Current:  snap.R(4),
		SnapType: "app",
	})
	snaptest.MockSnap(c, `name: some-snap`, si)

	ts, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Revision: snap.R(4)}, 0, snapstate.Flags{})
	c.Assert(err, IsNil)

	var snapsup snapstate.SnapSetup
	err = ts.Tasks()[0].Get("snap-setup", &snapsup)
	c.Assert(err, IsNil)
	c.Check(snapsup.Revision(), Equals, snap.R(4))
	c.Assert(s.fakeBackend.ops, HasLen, 0)
}

func (s *validationSetsSuite) TestUpdateToRevisionSnapRequiredByValidationWrongRevisionError(c *C) {
	const instanceKey = ""
	s.testUpdateToRevisionSnapRequiredByValidationWrongRevisionError(c, instanceKey)
}

func (s *validationSetsSuite) TestUpdateToRevisionSnapRequiredByValidationWrongRevisionErrorParallelInstall(c *C) {
	const instanceKey = "key"
	s.testUpdateToRevisionSnapRequiredByValidationWrongRevisionError(c, instanceKey)
}

func (s *validationSetsSuite) testUpdateToRevisionSnapRequiredByValidationWrongRevisionError(c *C, instanceKey string) {
	restore := snapstate.MockEnforcedValidationSets(func(st *state.State, extraVss ...*asserts.ValidationSet) (*snapasserts.ValidationSets, error) {
		vs := snapasserts.NewValidationSets()
		someSnap := map[string]any{
			"id":       "yOqKhntON3vR7kwEbVPsILm7bUViPDzx",
			"name":     "some-snap",
			"presence": "required",
			"revision": "5",
		}
		vsa1 := s.mockValidationSetAssert(c, "bar", "2", someSnap)
		vs.Add(vsa1.(*asserts.ValidationSet))
		return vs, nil
	})
	defer restore()

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

	if instanceKey != "" {
		tr := config.NewTransaction(s.state)
		tr.Set("core", "experimental.parallel-instances", true)
		tr.Commit()
	}

	tr := assertstate.ValidationSetTracking{
		AccountID: "foo",
		Name:      "bar",
		Mode:      assertstate.Enforce,
		Current:   2,
	}
	assertstate.UpdateValidationSet(s.state, &tr)

	const snapName = "some-snap"
	instanceName := snap.InstanceName(snapName, instanceKey)

	snapstate.Set(s.state, instanceName, &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: snapName, SnapID: "some-snap-id", Revision: snap.R(1)},
		}),
		Current:     snap.R(1),
		SnapType:    "app",
		InstanceKey: instanceKey,
	})
	_, err := snapstate.Update(s.state, instanceName, &snapstate.RevisionOptions{Revision: snap.R(11)}, 0, snapstate.Flags{})

	// we ignore validation sets when installing snaps with parallel instances
	if instanceKey == "" {
		c.Assert(err, ErrorMatches, fmt.Sprintf(`cannot update snap %q to revision 11 without --ignore-validation, revision 5 is required by validation sets: 16/foo/bar/2`, instanceName))
	} else {
		c.Assert(err, IsNil)
	}
}

// test that updating to a revision that is different than the revision required
// by a validation set is possible if --ignore-validation flag is passed.
func (s *validationSetsSuite) TestUpdateToWrongRevisionIgnoreValidation(c *C) {
	restore := snapstate.MockEnforcedValidationSets(func(st *state.State, extraVss ...*asserts.ValidationSet) (*snapasserts.ValidationSets, error) {
		vs := snapasserts.NewValidationSets()
		someSnap := map[string]any{
			"id":       "yOqKhntON3vR7kwEbVPsILm7bUViPDzx",
			"name":     "some-snap",
			"presence": "required",
			"revision": "5",
		}
		vsa1 := s.mockValidationSetAssert(c, "bar", "2", someSnap)
		vs.Add(vsa1.(*asserts.ValidationSet))
		return vs, nil
	})
	defer restore()

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

	tr := assertstate.ValidationSetTracking{
		AccountID: "foo",
		Name:      "bar",
		Mode:      assertstate.Enforce,
		Current:   2,
	}
	assertstate.UpdateValidationSet(s.state, &tr)

	// revision 1 is already installed; it doesn't match the required revision 5
	// but that's not relevant for the test (we could have installed it with
	// --ignore-validation before, and that's reflected by IgnoreValidation flag
	// in the snapstate).
	si := &snap.SideInfo{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)}
	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
		Current:  snap.R(1),
		SnapType: "app",
		Flags: snapstate.Flags{
			IgnoreValidation: true,
		},
	})
	snaptest.MockSnap(c, `name: some-snap`, si)

	refreshedDate := fakeRevDateEpoch.AddDate(0, 0, 1)

	// revision 5 is required and requesting revision 11 would fail
	// without --ignore-validation.
	revOpts := &snapstate.RevisionOptions{Revision: snap.R(11)}
	_, err := snapstate.Update(s.state, "some-snap", revOpts, 0, snapstate.Flags{IgnoreValidation: true})
	c.Assert(err, IsNil)

	c.Assert(s.fakeBackend.ops, HasLen, 2)
	expectedOps := fakeOps{{
		op: "storesvc-snap-action",
		curSnaps: []store.CurrentSnap{{
			InstanceName:     "some-snap",
			SnapID:           "some-snap-id",
			Revision:         snap.R(1),
			Epoch:            snap.E("1*"),
			RefreshedDate:    refreshedDate,
			IgnoreValidation: true,
			Resources:        make(map[string]snap.Revision),
		}},
	}, {
		op: "storesvc-snap-action:action",
		action: store.SnapAction{
			Action:       "refresh",
			InstanceName: "some-snap",
			SnapID:       "some-snap-id",
			Revision:     snap.R(11),
			Flags:        store.SnapActionIgnoreValidation,
		},
		revno: snap.R(11),
	}}
	c.Assert(s.fakeBackend.ops, DeepEquals, expectedOps)
}

func (s *validationSetsSuite) TestUpdateManyRequiredByValidationSetAlreadyAtCorrectRevisionNoop(c *C) {
	restore := snapstate.MockEnforcedValidationSets(func(st *state.State, extraVss ...*asserts.ValidationSet) (*snapasserts.ValidationSets, error) {
		vs := snapasserts.NewValidationSets()
		someSnap := map[string]any{
			"id":       "yOqKhntON3vR7kwEbVPsILm7bUViPDzx",
			"name":     "some-snap",
			"presence": "required",
			"revision": "5",
		}
		vsa1 := s.mockValidationSetAssert(c, "bar", "2", someSnap)
		vs.Add(vsa1.(*asserts.ValidationSet))
		return vs, nil
	})
	defer restore()

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

	tr := assertstate.ValidationSetTracking{
		AccountID: "foo",
		Name:      "bar",
		Mode:      assertstate.Enforce,
		Current:   2,
	}
	assertstate.UpdateValidationSet(s.state, &tr)

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(5)},
		}),
		Current:  snap.R(5),
		SnapType: "app",
	})
	names, _, err := snapstate.UpdateMany(context.Background(), s.state, []string{"some-snap"}, nil, 0, nil)
	c.Assert(err, IsNil)
	c.Check(names, HasLen, 0)
	c.Assert(s.fakeBackend.ops, HasLen, 0)
}

func (s *validationSetsSuite) TestUpdateManyRequiredByValidationSetsCohortIgnored(c *C) {
	restore := snapstate.MockEnforcedValidationSets(func(st *state.State, extraVs ...*asserts.ValidationSet) (*snapasserts.ValidationSets, error) {
		vs := snapasserts.NewValidationSets()
		someSnap := map[string]any{
			"id":       "yOqKhntON3vR7kwEbVPsILm7bUViPDzx",
			"name":     "some-snap",
			"presence": "required",
			"revision": "5",
		}
		vsa1 := s.mockValidationSetAssert(c, "bar", "2", someSnap)
		vs.Add(vsa1.(*asserts.ValidationSet))
		return vs, nil
	})
	defer restore()

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

	tr := assertstate.ValidationSetTracking{
		AccountID: "foo",
		Name:      "bar",
		Mode:      assertstate.Enforce,
		Current:   2,
	}
	assertstate.UpdateValidationSet(s.state, &tr)

	si := &snap.SideInfo{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)}
	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:    true,
		Sequence:  snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
		Current:   snap.R(1),
		SnapType:  "app",
		CohortKey: "cohortkey",
	})
	snaptest.MockSnap(c, `name: some-snap`, si)

	refreshedDate := fakeRevDateEpoch.AddDate(0, 0, 1)

	names, _, err := snapstate.UpdateMany(context.Background(), s.state, nil, nil, 0, &snapstate.Flags{})
	c.Assert(err, IsNil)
	c.Check(names, DeepEquals, []string{"some-snap"})

	c.Assert(s.fakeBackend.ops, HasLen, 2)
	expectedOps := fakeOps{{
		op: "storesvc-snap-action",
		curSnaps: []store.CurrentSnap{{
			InstanceName:  "some-snap",
			SnapID:        "some-snap-id",
			Revision:      snap.R(1),
			Epoch:         snap.E("1*"),
			RefreshedDate: refreshedDate,
			Resources:     make(map[string]snap.Revision),
		}},
	}, {
		op: "storesvc-snap-action:action",
		action: store.SnapAction{
			Action:         "refresh",
			InstanceName:   "some-snap",
			SnapID:         "some-snap-id",
			Revision:       snap.R(5),
			ValidationSets: []snapasserts.ValidationSetKey{"16/foo/bar/2"},
		},
		revno: snap.R(5),
	}}
	c.Assert(s.fakeBackend.ops, DeepEquals, expectedOps)
}

func (s *validationSetsSuite) TestUpdateManyRequiredByValidationSetIgnoreValidation(c *C) {
	restore := snapstate.MockEnforcedValidationSets(func(st *state.State, extraVss ...*asserts.ValidationSet) (*snapasserts.ValidationSets, error) {
		vs := snapasserts.NewValidationSets()
		someSnap := map[string]any{
			"id":       "yOqKhntON3vR7kwEbVPsILm7bUViPDzx",
			"name":     "some-snap",
			"presence": "required",
			"revision": "5",
		}
		vsa1 := s.mockValidationSetAssert(c, "bar", "2", someSnap)
		vs.Add(vsa1.(*asserts.ValidationSet))
		return vs, nil
	})
	defer restore()

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

	tr := assertstate.ValidationSetTracking{
		AccountID: "foo",
		Name:      "bar",
		Mode:      assertstate.Enforce,
		Current:   2,
	}
	assertstate.UpdateValidationSet(s.state, &tr)

	si := &snap.SideInfo{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)}
	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
		Current:  snap.R(1),
		SnapType: "app",
		Flags: snapstate.Flags{
			IgnoreValidation: true,
		},
	})
	snaptest.MockSnap(c, `name: some-snap`, si)

	refreshedDate := fakeRevDateEpoch.AddDate(0, 0, 1)
	names, _, err := snapstate.UpdateMany(context.Background(), s.state, nil, nil, 0, &snapstate.Flags{})
	c.Assert(err, IsNil)
	c.Check(names, DeepEquals, []string{"some-snap"})

	c.Assert(s.fakeBackend.ops, HasLen, 2)
	expectedOps := fakeOps{{
		op: "storesvc-snap-action",
		curSnaps: []store.CurrentSnap{{
			InstanceName:     "some-snap",
			SnapID:           "some-snap-id",
			Revision:         snap.R(1),
			Epoch:            snap.E("1*"),
			RefreshedDate:    refreshedDate,
			IgnoreValidation: true,
			Resources:        make(map[string]snap.Revision),
		}},
	}, {
		op: "storesvc-snap-action:action",
		action: store.SnapAction{
			Action:       "refresh",
			InstanceName: "some-snap",
			SnapID:       "some-snap-id",
			Flags:        store.SnapActionIgnoreValidation,
		},
		revno: snap.R(11),
	}}
	c.Assert(s.fakeBackend.ops, DeepEquals, expectedOps)
}

func (s *validationSetsSuite) TestUpdateSnapRequiredByValidationSetAlreadyAtRequiredRevisionIgnoreValidationOK(c *C) {
	restore := snapstate.MockEnforcedValidationSets(func(st *state.State, extraVss ...*asserts.ValidationSet) (*snapasserts.ValidationSets, error) {
		vs := snapasserts.NewValidationSets()
		someSnap := map[string]any{
			"id":       "yOqKhntON3vR7kwEbVPsILm7bUViPDzx",
			"name":     "some-snap",
			"presence": "required",
			"revision": "4",
		}
		vsa1 := s.mockValidationSetAssert(c, "bar", "1", someSnap)
		vs.Add(vsa1.(*asserts.ValidationSet))
		return vs, nil
	})
	defer restore()

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

	tr := assertstate.ValidationSetTracking{
		AccountID: "foo",
		Name:      "bar",
		Mode:      assertstate.Enforce,
		Current:   1,
	}
	assertstate.UpdateValidationSet(s.state, &tr)

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(4)},
		}),
		Current:  snap.R(4),
		SnapType: "app",
	})

	// this would normally fail since the snap is already installed at the required revision 4; will get
	// refreshed to revision 11.
	_, err := snapstate.Update(s.state, "some-snap", nil, 0, snapstate.Flags{IgnoreValidation: true})
	c.Assert(err, IsNil)
	c.Assert(s.fakeBackend.ops, HasLen, 2)
	expectedOp := fakeOp{
		op: "storesvc-snap-action:action",
		action: store.SnapAction{
			Action:       "refresh",
			InstanceName: "some-snap",
			SnapID:       "some-snap-id",
			Flags:        store.SnapActionIgnoreValidation,
		},
		revno: snap.R(11),
	}
	c.Assert(s.fakeBackend.ops[1], DeepEquals, expectedOp)
}

func (s *validationSetsSuite) TestUpdateToRevisionWithValidationSets(c *C) {
	restore := snapstate.MockEnforcedValidationSets(func(st *state.State, extraVss ...*asserts.ValidationSet) (*snapasserts.ValidationSets, error) {
		return nil, fmt.Errorf("unexpected")
	})
	defer restore()

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

	si := &snap.SideInfo{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)}
	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
		Current:  snap.R(1),
		SnapType: "app",
	})
	snaptest.MockSnap(c, `name: some-snap`, si)

	refreshedDate := fakeRevDateEpoch.AddDate(0, 0, 1)

	vsets := snapasserts.NewValidationSets()
	vsets.Add(s.mockValidationSetAssert(c, "bar", "1", map[string]any{
		"id":       "yOqKhntON3vR7kwEbVPsILm7bUViPDzx",
		"name":     "some-snap",
		"presence": "required",
	}).(*asserts.ValidationSet))
	vsets.Add(s.mockValidationSetAssert(c, "baz", "2", map[string]any{
		"id":       "yOqKhntON3vR7kwEbVPsILm7bUViPDzx",
		"name":     "some-snap",
		"presence": "required",
		"revision": "11",
	}).(*asserts.ValidationSet))

	ts, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Revision: snap.R(11), ValidationSets: vsets}, 0, snapstate.Flags{})
	c.Assert(err, IsNil)

	var snapsup snapstate.SnapSetup
	err = ts.Tasks()[0].Get("snap-setup", &snapsup)
	c.Assert(err, IsNil)

	// new snap revision from the store
	c.Check(snapsup.Revision(), Equals, snap.R(11))

	c.Assert(s.fakeBackend.ops, HasLen, 2)
	expectedOps := fakeOps{{
		op: "storesvc-snap-action",
		curSnaps: []store.CurrentSnap{{
			InstanceName:  "some-snap",
			SnapID:        "some-snap-id",
			Revision:      snap.R(1),
			Epoch:         snap.E("1*"),
			RefreshedDate: refreshedDate,
			Resources:     make(map[string]snap.Revision),
		}}}, {
		op: "storesvc-snap-action:action",
		action: store.SnapAction{
			Action:         "refresh",
			InstanceName:   "some-snap",
			SnapID:         "some-snap-id",
			Revision:       snap.R(11),
			ValidationSets: vsets.Keys(),
		},
		revno: snap.R(11),
	}}
	c.Assert(s.fakeBackend.ops, DeepEquals, expectedOps)
}

func (s *validationSetsSuite) TestUpdateWithValidationSetsInvalidSnap(c *C) {
	restore := snapstate.MockEnforcedValidationSets(func(st *state.State, extraVss ...*asserts.ValidationSet) (*snapasserts.ValidationSets, error) {
		return nil, fmt.Errorf("unexpected")
	})
	defer restore()

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

	si := &snap.SideInfo{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)}
	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
		Current:  snap.R(1),
		SnapType: "app",
	})
	snaptest.MockSnap(c, `name: some-snap`, si)

	vsets := snapasserts.NewValidationSets()
	vsets.Add(s.mockValidationSetAssert(c, "bar", "1", map[string]any{
		"id":       "yOqKhntON3vR7kwEbVPsILm7bUViPDzx",
		"name":     "some-snap",
		"presence": "invalid",
	}).(*asserts.ValidationSet))

	_, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Revision: snap.R(11), ValidationSets: vsets}, 0, snapstate.Flags{})
	c.Assert(err, ErrorMatches, `cannot update snap "some-snap" due to enforcing rules of validation set 16/foo/bar/1`)
}

func (s *snapmgrTestSuite) TestUpdatePrerequisiteWithSameDeviceContext(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	snapstate.Set(s.state, "outdated-producer", &snapstate.SnapState{
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{
			RealName: "outdated-producer",
			SnapID:   "outdated-producer-id",
			Revision: snap.R(1),
		}}),
		Current: snap.R(1),
		Active:  true,
	})
	snapstate.Set(s.state, "outdated-consumer", &snapstate.SnapState{
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{
			RealName: "outdated-consumer",
			SnapID:   "outdated-consumer-id",
			Revision: snap.R(1),
		}}),
		Current: snap.R(1),
		Active:  true,
	})

	// unset the global store, it will need to come via the device context
	snapstate.ReplaceStore(s.state, nil)

	deviceCtx := &snapstatetest.TrivialDeviceContext{
		CtxStore: contentStore{
			fakeStore: s.fakeStore,
			state:     s.state,
		},
		DeviceModel: &asserts.Model{},
	}
	snapstatetest.MockDeviceContext(deviceCtx)

	ts, err := snapstate.UpdateWithDeviceContext(s.state, "outdated-consumer", nil, s.user.ID, snapstate.Flags{NoReRefresh: true}, nil, deviceCtx, "")
	c.Assert(err, IsNil)
	c.Assert(ts.Tasks(), Not(HasLen), 0)

	chg := s.state.NewChange("update", "test: update")
	chg.AddAll(ts)

	s.settle(c)

	c.Check(s.fakeStore.downloads, DeepEquals, []fakeDownload{
		{macaroon: s.user.StoreMacaroon, name: "outdated-consumer", target: filepath.Join(dirs.SnapBlobDir, "outdated-consumer_11.snap")},
		{macaroon: s.user.StoreMacaroon, name: "outdated-producer", target: filepath.Join(dirs.SnapBlobDir, "outdated-producer_11.snap")},
	})
}

func (s *validationSetsSuite) testUpdateManyValidationSetsPartialFailure(c *C) *state.Change {
	logbuf, rest := logger.MockLogger()
	defer rest()

	s.fakeStore.refreshRevnos = map[string]snap.Revision{
		"aaqKhntON3vR7kwEbVPsILm7bUViPDz":  snap.R(11),
		"bgtKhntON3vR7kwEbVPsILm7bUViPDzx": snap.R(11),
	}

	var enforcedValidationSetsCalls int
	restore := snapstate.MockEnforcedValidationSets(func(st *state.State, extraVss ...*asserts.ValidationSet) (*snapasserts.ValidationSets, error) {
		vs := snapasserts.NewValidationSets()
		snap1 := map[string]any{
			"id":       "aaqKhntON3vR7kwEbVPsILm7bUViPDzx",
			"name":     "some-snap",
			"presence": "required",
		}
		snap2 := map[string]any{
			"id":       "bgtKhntON3vR7kwEbVPsILm7bUViPDzx",
			"name":     "some-other-snap",
			"presence": "required",
		}
		var sequence string
		if enforcedValidationSetsCalls == 0 {
			snap1["revision"] = "11"
			snap2["revision"] = "11"
			sequence = "3"
		} else {
			snap1["revision"] = "1"
			snap2["revision"] = "1"
			sequence = "2"
		}
		vsa1 := s.mockValidationSetAssert(c, "bar", sequence, snap1, snap2)
		c.Assert(vs.Add(vsa1.(*asserts.ValidationSet)), IsNil)
		enforcedValidationSetsCalls++
		return vs, nil
	})
	defer restore()

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

	tr := assertstate.ValidationSetTracking{
		AccountID: "foo",
		Name:      "bar",
		Mode:      assertstate.Enforce,
		Current:   2,
	}
	assertstate.UpdateValidationSet(s.state, &tr)

	si1 := &snap.SideInfo{RealName: "some-snap", SnapID: "aaqKhntON3vR7kwEbVPsILm7bUViPDzx", Revision: snap.R(1)}
	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si1}),
		Current:  snap.R(1),
		SnapType: "app",
	})
	snaptest.MockSnap(c, `name: some-snap`, si1)

	si2 := &snap.SideInfo{RealName: "some-other-snap", SnapID: "bgtKhntON3vR7kwEbVPsILm7bUViPDzx", Revision: snap.R(1)}
	snapstate.Set(s.state, "some-other-snap", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si2}),
		Current:  snap.R(1),
		SnapType: "app",
	})
	snaptest.MockSnap(c, `name: some-other-snap`, si2)

	s.fakeBackend.linkSnapFailTrigger = filepath.Join(dirs.SnapMountDir, "/some-other-snap/11")

	names, tss, err := snapstate.UpdateMany(context.Background(), s.state, nil, nil, s.user.ID, &snapstate.Flags{})
	c.Assert(err, IsNil)
	c.Check(names, DeepEquals, []string{"some-other-snap", "some-snap"})
	c.Check(logbuf.String(), Equals, "")
	chg := s.state.NewChange("update", "")
	for _, ts := range tss {
		chg.AddAll(ts)
	}

	s.settle(c)

	return chg
}

func (s *validationSetsSuite) TestUpdateManyValidationSetsPartialFailureNothingToRestore(c *C) {
	var refreshed []string
	restoreMaybeRestoreValidationSetsAndRevertSnaps := snapstate.MockMaybeRestoreValidationSetsAndRevertSnaps(func(st *state.State, refreshedSnaps []string, fromChange string) ([]*state.TaskSet, error) {
		refreshed = refreshedSnaps
		// nothing to restore
		return nil, nil
	})
	defer restoreMaybeRestoreValidationSetsAndRevertSnaps()

	var addCurrentTrackingToValidationSetsStackCalled int
	restoreAddCurrentTrackingToValidationSetsStack := snapstate.MockAddCurrentTrackingToValidationSetsStack(func(st *state.State) error {
		addCurrentTrackingToValidationSetsStackCalled++
		return nil
	})
	defer restoreAddCurrentTrackingToValidationSetsStack()

	s.testUpdateManyValidationSetsPartialFailure(c)

	// only some-snap was successfully refreshed, this also confirms that
	// mockMaybeRestoreValidationSetsAndRevertSnaps was called.
	c.Check(refreshed, DeepEquals, []string{"some-snap"})

	// validation sets history update was attempted (could be a no-op if
	// maybeRestoreValidationSetsAndRevertSnaps restored last tracking
	// data).
	c.Check(addCurrentTrackingToValidationSetsStackCalled, Equals, 1)
}

func (s *validationSetsSuite) TestUpdateManyValidationSetsPartialFailureRevertTasks(c *C) {
	restore := snapstate.MockRestoreValidationSetsTracking(func(st *state.State) error {
		tr := assertstate.ValidationSetTracking{
			AccountID: "foo",
			Name:      "bar",
			Mode:      assertstate.Enforce,
			Current:   2,
		}
		assertstate.UpdateValidationSet(s.state, &tr)
		return nil
	})
	defer restore()

	chg := s.testUpdateManyValidationSetsPartialFailure(c)

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

	seenLinkSnap := make(map[string]int)
	var checkReRefreshTask *state.Task
	for _, t := range chg.Tasks() {
		if t.Kind() == "check-rerefresh" {
			checkReRefreshTask = t
		}
		if t.Kind() == "link-snap" {
			sup, err := snapstate.TaskSnapSetup(t)
			c.Assert(err, IsNil)
			if sup.SnapName() == "some-snap" && t.Status() == state.DoneStatus {
				c.Assert(t.Status(), Equals, state.DoneStatus)
			}
			// some-other-snap failed to refresh
			if sup.SnapName() == "some-other-snap" {
				c.Assert(t.Status(), Equals, state.ErrorStatus)
			}
			seenLinkSnap[fmt.Sprintf("%s:%s", sup.SnapName(), sup.Revision())]++
		}
	}

	// some-snap was seen twice, first time was successful refresh, second time was for
	// the revert to previous revision
	c.Check(seenLinkSnap, DeepEquals, map[string]int{
		"some-snap:11": 1,
		"some-snap:1":  1,
		// some-other-snap failed
		"some-other-snap:11": 1,
	})

	var snapSt snapstate.SnapState
	// both snap are at the initial revisions
	c.Assert(snapstate.Get(s.state, "some-snap", &snapSt), IsNil)
	c.Check(snapSt.Current, Equals, snap.R("1"))
	c.Assert(snapstate.Get(s.state, "some-other-snap", &snapSt), IsNil)
	c.Check(snapSt.Current, Equals, snap.R("1"))

	c.Check(chg.Status(), Equals, state.ErrorStatus)
	c.Assert(checkReRefreshTask, NotNil)
	c.Check(checkReRefreshTask.Status(), Equals, state.DoneStatus)
}

func (s *snapmgrTestSuite) TestUpdatePrerequisiteBackwardsCompat(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	snapstate.Set(s.state, "outdated-producer", &snapstate.SnapState{
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{
			RealName: "outdated-producer",
			SnapID:   "outdated-producer-id",
			Revision: snap.R(1),
		}}),
		Current: snap.R(1),
		Active:  true,
	})
	snapstate.Set(s.state, "outdated-consumer", &snapstate.SnapState{
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{
			RealName: "outdated-consumer",
			SnapID:   "outdated-consumer-id",
			Revision: snap.R(1),
		}}),
		Current: snap.R(1),
		Active:  true,
	})

	tasks, err := snapstate.Update(s.state, "outdated-consumer", nil, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)
	c.Check(tasks, Not(HasLen), 0)
	chg := s.state.NewChange("update", "test: update snap")
	chg.AddAll(tasks)

	earlyPrereq, syncPrereq := findPrereqTasksForSnap(c, chg, "outdated-consumer")

	// mimic tasks serialized by an "old" snapd without PrereqContentAttrs.
	// The new code shouldn't update the prereq since it doesn't have the content attrs.
	var snapsup snapstate.SnapSetup
	err = earlyPrereq.Get("snap-setup", &snapsup)
	c.Assert(err, IsNil)
	c.Assert(snapsup.InstanceName(), Equals, "outdated-consumer")
	snapsup.PrereqContentAttrs = nil
	earlyPrereq.Set("snap-setup", &snapsup)

	// the sync prerequisites task also needs to be updated, since it points to
	// a different snap setup
	err = snapstate.SetTaskSnapSetup(syncPrereq, &snapsup)
	c.Assert(err, IsNil)

	s.settle(c)

	// the producer wasn't updated since there were no content attributes
	c.Check(s.fakeStore.downloads, DeepEquals, []fakeDownload{
		{macaroon: s.user.StoreMacaroon, name: "outdated-consumer", target: filepath.Join(dirs.SnapBlobDir, "outdated-consumer_11.snap")},
	})
}

func (s *snapmgrTestSuite) TestUpdateDeduplicatesSnapNames(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{
			RealName: "some-snap",
			SnapID:   "some-snap-id",
			Revision: snap.R(1),
		}}),
		Current: snap.R(1),
		Active:  true,
	})

	snapstate.Set(s.state, "some-base", &snapstate.SnapState{
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{
			RealName: "some-base",
			SnapID:   "some-base-id",
			Revision: snap.R(1),
		}}),
		Current: snap.R(1),
		Active:  true,
	})

	updated, _, err := snapstate.UpdateMany(context.Background(), s.state, []string{"some-snap", "some-base", "some-snap", "some-base"}, nil, s.user.ID, nil)
	c.Assert(err, IsNil)
	c.Check(updated, testutil.DeepUnsortedMatches, []string{"some-snap", "some-base"})
}

func (s *snapmgrTestSuite) TestUpdateDoHiddenDirMigration(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	tr := config.NewTransaction(s.state)
	c.Assert(tr.Set("core", "experimental.hidden-snap-folder", true), IsNil)
	tr.Commit()

	info := &snap.SideInfo{
		Revision: snap.R(1),
		SnapID:   "some-snap-id",
		RealName: "some-snap",
	}
	snapst := &snapstate.SnapState{
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{info}),
		Current:  info.Revision,
		Active:   true,
	}
	snapstate.Set(s.state, "some-snap", snapst)
	c.Assert(snapstate.WriteSeqFile("some-snap", snapst), IsNil)

	chg := s.state.NewChange("update", "update a snap")
	ts, err := snapstate.Update(s.state, "some-snap", nil, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)
	chg.AddAll(ts)

	s.settle(c)
	c.Assert(chg.Err(), IsNil)
	c.Assert(chg.Status(), Equals, state.DoneStatus)

	// check backend hid data
	s.fakeBackend.ops.MustFindOp(c, "hide-snap-data")
	c.Assert(s.fakeBackend.ops.First("init-exposed-snap-home"), IsNil)

	// check state and seq file were updated
	expected := &dirs.SnapDirOptions{HiddenSnapDataDir: true}
	assertMigrationState(c, s.state, "some-snap", expected)
}

func (s *snapmgrTestSuite) TestUndoMigrationIfUpdateFailsAfterSettingState(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	tr := config.NewTransaction(s.state)
	c.Assert(tr.Set("core", "experimental.hidden-snap-folder", true), IsNil)
	tr.Commit()

	info := &snap.SideInfo{
		Revision: snap.R(1),
		SnapID:   "some-snap-id",
		RealName: "some-snap",
	}
	snapst := &snapstate.SnapState{
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{info}),
		Current:  info.Revision,
		Active:   true,
	}
	snapstate.Set(s.state, "some-snap", snapst)
	c.Assert(snapstate.WriteSeqFile("some-snap", snapst), IsNil)

	chg := s.state.NewChange("update", "update a snap")
	ts, err := snapstate.Update(s.state, "some-snap", nil, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)
	chg.AddAll(ts)

	// fail the change after the link-snap task (after state is saved)
	s.o.TaskRunner().AddHandler("fail", func(*state.Task, *tomb.Tomb) error {
		return errors.New("expected")
	}, nil)

	failingTask := s.state.NewTask("fail", "expected failure")
	chg.AddTask(failingTask)
	linkTask := findLastTask(chg, "link-snap")
	failingTask.WaitFor(linkTask)
	for _, lane := range linkTask.Lanes() {
		failingTask.JoinLane(lane)
	}

	s.settle(c)
	c.Assert(chg.Err(), Not(IsNil))

	// check migration is undone
	s.fakeBackend.ops.MustFindOp(c, "hide-snap-data")
	s.fakeBackend.ops.MustFindOp(c, "undo-hide-snap-data")

	// check migration status was reverted in state and seq file
	assertMigrationState(c, s.state, "some-snap", nil)
}

func (s *snapmgrTestSuite) TestUndoMigrationIfUpdateFails(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	tr := config.NewTransaction(s.state)
	c.Assert(tr.Set("core", "experimental.hidden-snap-folder", true), IsNil)
	tr.Commit()

	info := &snap.SideInfo{
		Revision: snap.R(1),
		SnapID:   "some-snap-id",
		RealName: "some-snap",
	}
	snapst := &snapstate.SnapState{
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{info}),
		Current:  info.Revision,
		Active:   true,
	}
	snapstate.Set(s.state, "some-snap", snapst)
	c.Assert(snapstate.WriteSeqFile("some-snap", snapst), IsNil)

	// fail at the end
	s.fakeBackend.linkSnapFailTrigger = filepath.Join(dirs.SnapMountDir, "/some-snap/11")

	chg := s.state.NewChange("update", "update a snap")
	ts, err := snapstate.Update(s.state, "some-snap", nil, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)
	chg.AddAll(ts)

	s.settle(c)
	c.Assert(chg.Err(), Not(IsNil))

	// check migration is undone
	s.fakeBackend.ops.MustFindOp(c, "hide-snap-data")
	s.fakeBackend.ops.MustFindOp(c, "undo-hide-snap-data")

	// check migration is off in state and seq file
	assertMigrationState(c, s.state, "some-snap", nil)
}

func (s *snapmgrTestSuite) TestUpdateAfterMigration(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	tr := config.NewTransaction(s.state)
	c.Assert(tr.Set("core", "experimental.hidden-snap-folder", true), IsNil)
	tr.Commit()

	info := &snap.SideInfo{
		Revision: snap.R(1),
		SnapID:   "some-snap-id",
		RealName: "some-snap",
	}
	snapst := &snapstate.SnapState{
		Sequence:       snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{info}),
		Current:        info.Revision,
		Active:         true,
		MigratedHidden: true,
	}
	snapstate.Set(s.state, "some-snap", snapst)
	c.Assert(snapstate.WriteSeqFile("some-snap", snapst), IsNil)

	chg := s.state.NewChange("update", "update a snap")
	ts, err := snapstate.Update(s.state, "some-snap", nil, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)
	chg.AddAll(ts)

	s.settle(c)
	c.Assert(chg.Err(), IsNil)
	c.Assert(chg.Status(), Equals, state.DoneStatus)

	// shouldn't do migration since it was already done
	c.Assert(s.fakeBackend.ops.First("hide-snap-data"), IsNil)
	c.Assert(s.fakeBackend.ops.First("undo-hide-snap-data"), IsNil)

	expected := &dirs.SnapDirOptions{HiddenSnapDataDir: true}
	c.Assert(s.fakeBackend.ops.MustFindOp(c, "copy-data").dirOpts, DeepEquals, expected)

	assertMigrationState(c, s.state, "some-snap", expected)
}

func (s *snapmgrTestSuite) TestUpdateAfterCore22Migration(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	tr := config.NewTransaction(s.state)
	c.Assert(tr.Set("core", "experimental.hidden-snap-folder", true), IsNil)
	tr.Commit()

	info := &snap.SideInfo{
		Revision: snap.R(1),
		SnapID:   "some-snap-id",
		RealName: "some-snap",
	}
	snapst := &snapstate.SnapState{
		Sequence:              snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{info}),
		Current:               info.Revision,
		Active:                true,
		MigratedHidden:        true,
		MigratedToExposedHome: true,
	}
	snapstate.Set(s.state, "some-snap", snapst)
	c.Assert(snapstate.WriteSeqFile("some-snap", snapst), IsNil)

	chg := s.state.NewChange("update", "update a snap")
	ts, err := snapstate.Update(s.state, "some-snap", nil, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)
	chg.AddAll(ts)

	s.settle(c)
	c.Assert(chg.Err(), IsNil)
	c.Assert(chg.Status(), Equals, state.DoneStatus)

	// shouldn't do migration since it was already done
	c.Check(s.fakeBackend.ops.First("hide-snap-data"), IsNil)
	c.Check(s.fakeBackend.ops.First("undo-hide-snap-data"), IsNil)
	c.Check(s.fakeBackend.ops.First("init-exposed-snap-home"), IsNil)
	c.Check(s.fakeBackend.ops.First("undo-init-exposed-snap-home"), IsNil)

	expected := &dirs.SnapDirOptions{HiddenSnapDataDir: true, MigratedToExposedHome: true}
	c.Check(s.fakeBackend.ops.MustFindOp(c, "copy-data").dirOpts, DeepEquals, expected)

	assertMigrationState(c, s.state, "some-snap", expected)
}

// takes in some test parameters to prepare the failure and return the root
// error that will cause the failure.
type prepFailFunc func(*overlord.Overlord, *state.Change) error

func (s *snapmgrTestSuite) TestUndoRevertMigrationIfRevertFails(c *C) {
	s.testUndoRevertMigrationIfRevertFails(c, func(_ *overlord.Overlord, chg *state.Change) error {
		err := errors.New("boom")
		s.fakeBackend.maybeInjectErr = func(op *fakeOp) error {
			if op.op == "undo-hide-snap-data" {
				return err
			}

			return nil
		}

		return err
	})
}

func (s *snapmgrTestSuite) TestUndoRevertMigrationIfRevertFailsAfterWritingState(c *C) {
	// fail the change after the link-snap task (after state is saved)
	s.testUndoRevertMigrationIfRevertFails(c, failAfterLinkSnap)
}

func (s *snapmgrTestSuite) testUndoRevertMigrationIfRevertFails(c *C, prepFail prepFailFunc) {
	s.state.Lock()
	defer s.state.Unlock()

	info := &snap.SideInfo{
		Revision: snap.R(1),
		SnapID:   "some-snap-id",
		RealName: "some-snap",
	}

	snapst := &snapstate.SnapState{
		Sequence:       snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{info}),
		Current:        info.Revision,
		Active:         true,
		MigratedHidden: true,
	}
	snapstate.Set(s.state, "some-snap", snapst)
	c.Assert(snapstate.WriteSeqFile("some-snap", snapst), IsNil)

	chg := s.state.NewChange("update", "update a snap")
	ts, err := snapstate.Update(s.state, "some-snap", nil, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)
	chg.AddAll(ts)

	expectedErr := prepFail(s.o, chg)

	s.settle(c)
	c.Assert(chg.Status(), Equals, state.ErrorStatus)
	c.Assert(chg.Err(), ErrorMatches, fmt.Sprintf(`(.|\s)*%s\)?`, expectedErr.Error()))

	s.fakeBackend.ops.MustFindOp(c, "undo-hide-snap-data")

	// check migration reversion was undone in state and seq file
	expectedOpts := &dirs.SnapDirOptions{HiddenSnapDataDir: true}
	assertMigrationState(c, s.state, "some-snap", expectedOpts)
}

func containsInOrder(c *C, ops fakeOps, expected []string) {
	var i int
	opNames := make([]string, len(ops))
	for i, op := range ops {
		opNames[i] = op.op
	}

	for _, op := range opNames {
		if op == expected[i] {
			i++

			// found all ops
			if i == len(expected) {
				return
			}
		}
	}

	c.Fatalf("cannot find 1st sequence in 2nd:\n%q\n%q", expected, opNames)
}

func (s *snapmgrTestSuite) TestRevertMigration(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	info := &snap.SideInfo{
		Revision: snap.R(1),
		SnapID:   "some-snap-id",
		RealName: "some-snap",
	}

	snapst := &snapstate.SnapState{
		Sequence:       snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{info}),
		Current:        info.Revision,
		Active:         true,
		MigratedHidden: true,
	}
	snapstate.Set(s.state, "some-snap", snapst)
	c.Assert(snapstate.WriteSeqFile("some-snap", snapst), IsNil)

	chg := s.state.NewChange("update", "update a snap")
	ts, err := snapstate.Update(s.state, "some-snap", nil, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)
	chg.AddAll(ts)

	s.settle(c)
	c.Assert(chg.Err(), IsNil)

	s.fakeBackend.ops.MustFindOp(c, "undo-hide-snap-data")

	// check migration status is 'off' in state and seq file
	assertMigrationState(c, s.state, "some-snap", nil)
}

func (s *snapmgrTestSuite) TestUpdateDoHiddenDirMigrationOnCore22(c *C) {
	c.Skip("TODO:Snap-folder: no automatic migration for core22 snaps to ~/Snap folder for now")

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

	si := &snap.SideInfo{
		Revision: snap.R(1),
		SnapID:   "snap-core18-to-core22-id",
		RealName: "snap-core18-to-core22",
	}
	snapst := &snapstate.SnapState{
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
		Current:  si.Revision,
		Active:   true,
	}
	snapstate.Set(s.state, "snap-core18-to-core22", snapst)
	c.Assert(snapstate.WriteSeqFile("snap-core18-to-core22", snapst), IsNil)

	chg := s.state.NewChange("update", "update a snap")
	ts, err := snapstate.Update(s.state, "snap-core18-to-core22", &snapstate.RevisionOptions{Channel: "channel-for-core22/stable"}, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)
	chg.AddAll(ts)

	s.settle(c)
	c.Assert(chg.Err(), IsNil)
	c.Assert(chg.Status(), Equals, state.DoneStatus)

	c.Check(s.fakeStore.downloads, DeepEquals, []fakeDownload{
		{macaroon: s.user.StoreMacaroon, name: "core22", target: filepath.Join(dirs.SnapBlobDir, "core22_11.snap")},
		{macaroon: s.user.StoreMacaroon, name: "snap-core18-to-core22", target: filepath.Join(dirs.SnapBlobDir, "snap-core18-to-core22_2.snap")},
	})

	containsInOrder(c, s.fakeBackend.ops, []string{"hide-snap-data", "init-exposed-snap-home"})

	expected := &dirs.SnapDirOptions{HiddenSnapDataDir: true, MigratedToExposedHome: true}
	assertMigrationState(c, s.state, "snap-core18-to-core22", expected)
}

func (s *snapmgrTestSuite) TestUndoMigrationIfUpdateToCore22FailsAfterWritingState(c *C) {
	c.Skip("TODO:Snap-folder: no automatic migration for core22 snaps to ~/Snap folder for now")

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

	si := &snap.SideInfo{
		Revision: snap.R(1),
		SnapID:   "snap-core18-to-core22-id",
		RealName: "snap-core18-to-core22",
	}
	snapst := &snapstate.SnapState{
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
		Current:  si.Revision,
		Active:   true,
	}
	snapstate.Set(s.state, "snap-core18-to-core22", snapst)
	c.Assert(snapstate.WriteSeqFile("snap-core18-to-core22", snapst), IsNil)

	// adding core22 so it's easier to find the other snap's tasks below
	snapstate.Set(s.state, "core22", &snapstate.SnapState{
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{
			SnapID:   "core22",
			Revision: snap.R("2"),
			RealName: "core22",
		}}),
		Current:  snap.R("2"),
		SnapType: "base",
	})

	chg := s.state.NewChange("update", "update a snap")
	ts, err := snapstate.Update(s.state, "snap-core18-to-core22", &snapstate.RevisionOptions{Channel: "channel-for-core22/stable"}, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)
	chg.AddAll(ts)

	// fails change while after link-snap runs (after state is persisted)
	expectedErr := failAfterLinkSnap(s.o, chg)

	s.settle(c)
	c.Assert(chg.Err(), Not(IsNil))
	c.Assert(chg.Status(), Equals, state.ErrorStatus)
	c.Assert(chg.Err(), ErrorMatches, fmt.Sprintf(`(.|\s)*%s\)?`, expectedErr.Error()))

	expectedOps := []string{"hide-snap-data", "init-exposed-snap-home", "undo-init-exposed-snap-home", "undo-hide-snap-data"}
	containsInOrder(c, s.fakeBackend.ops, expectedOps)

	// check that the undoInfo returned by InitExposed and stored in the task is
	// the same as the one passed into UndoInitExposed
	t := findLastTask(chg, "copy-snap-data")
	c.Assert(t, Not(IsNil))

	var undoInfo backend.UndoInfo
	c.Assert(t.Get("undo-exposed-home-init", &undoInfo), IsNil)
	op := s.fakeBackend.ops.MustFindOp(c, "undo-init-exposed-snap-home")
	c.Check(op.undoInfo, DeepEquals, &undoInfo)

	assertMigrationState(c, s.state, "snap-core18-to-core22", nil)
}

func (s *snapmgrTestSuite) TestUndoMigrationIfUpdateToCore22Fails(c *C) {
	c.Skip("TODO:Snap-folder: no automatic migration for core22 snaps to ~/Snap folder for now")

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

	si := &snap.SideInfo{
		Revision: snap.R(1),
		SnapID:   "snap-core18-to-core22-id",
		RealName: "snap-core18-to-core22",
	}
	snapst := &snapstate.SnapState{
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
		Current:  si.Revision,
		Active:   true,
	}
	snapstate.Set(s.state, "snap-core18-to-core22", snapst)
	c.Assert(snapstate.WriteSeqFile("snap-core18-to-core22", snapst), IsNil)

	chg := s.state.NewChange("update", "update a snap")
	ts, err := snapstate.Update(s.state, "snap-core18-to-core22", &snapstate.RevisionOptions{Channel: "channel-for-core22/stable"}, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)
	chg.AddAll(ts)

	// fails change while initializing ~/Snap (before state is persisted)
	expectedErr := errors.New("boom")
	s.fakeBackend.maybeInjectErr = func(op *fakeOp) error {
		if op.op == "init-exposed-snap-home" {
			return expectedErr
		}
		return nil
	}

	s.settle(c)
	c.Assert(chg.Status(), Equals, state.ErrorStatus)
	c.Assert(chg.Err(), ErrorMatches, fmt.Sprintf(`(.|\s)*%s\)?`, expectedErr.Error()))

	expectedOps := []string{"hide-snap-data", "init-exposed-snap-home"}
	containsInOrder(c, s.fakeBackend.ops, expectedOps)

	assertMigrationState(c, s.state, "snap-core18-to-core22", nil)
}

func (s *snapmgrTestSuite) TestUpdateMigrateTurnOffFlagAndRefreshToCore22(c *C) {
	c.Skip("TODO:Snap-folder: no automatic migration for core22 snaps to ~/Snap folder for now")

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

	si := &snap.SideInfo{
		Revision: snap.R(1),
		SnapID:   "snap-core18-to-core22-id",
		RealName: "snap-core18-to-core22",
	}
	snapst := &snapstate.SnapState{
		Sequence:       snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
		Current:        si.Revision,
		Active:         true,
		MigratedHidden: true,
	}
	// state is hidden but flag is off; was turned off just before refresh to
	// core22 base
	snapstate.Set(s.state, "snap-core18-to-core22", snapst)
	c.Assert(snapstate.WriteSeqFile("snap-core18-to-core22", snapst), IsNil)

	chg := s.state.NewChange("update", "update a snap")
	ts, err := snapstate.Update(s.state, "snap-core18-to-core22", &snapstate.RevisionOptions{Channel: "channel-for-core22/stable"}, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)
	chg.AddAll(ts)

	s.settle(c)
	c.Assert(chg.Err(), IsNil)
	c.Assert(chg.Status(), Equals, state.DoneStatus)

	c.Check(s.fakeStore.downloads, DeepEquals, []fakeDownload{
		{macaroon: s.user.StoreMacaroon, name: "core22", target: filepath.Join(dirs.SnapBlobDir, "core22_11.snap")},
		{macaroon: s.user.StoreMacaroon, name: "snap-core18-to-core22", target: filepath.Join(dirs.SnapBlobDir, "snap-core18-to-core22_2.snap")},
	})

	containsInOrder(c, s.fakeBackend.ops, []string{"init-exposed-snap-home", "init-xdg-dirs"})

	expected := &dirs.SnapDirOptions{HiddenSnapDataDir: true, MigratedToExposedHome: true}
	assertMigrationState(c, s.state, "snap-core18-to-core22", expected)
}

func (s *snapmgrTestSuite) TestUpdateMigrateTurnOffFlagAndRefreshToCore22ButFail(c *C) {
	c.Skip("TODO:Snap-folder: no automatic migration for core22 snaps to ~/Snap folder for now")

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

	si := &snap.SideInfo{
		Revision: snap.R(1),
		SnapID:   "snap-core18-to-core22-id",
		RealName: "snap-core18-to-core22",
	}
	snapst := &snapstate.SnapState{
		Sequence:       snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
		Current:        si.Revision,
		Active:         true,
		MigratedHidden: true,
	}
	// state is hidden but flag is off; was turned off just before refresh to
	// core22 base
	snapstate.Set(s.state, "snap-core18-to-core22", snapst)
	c.Assert(snapstate.WriteSeqFile("snap-core18-to-core22", snapst), IsNil)

	chg := s.state.NewChange("update", "update a snap")
	ts, err := snapstate.Update(s.state, "snap-core18-to-core22", &snapstate.RevisionOptions{Channel: "channel-for-core22/stable"}, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)
	chg.AddAll(ts)

	s.fakeBackend.maybeInjectErr = func(op *fakeOp) error {
		if op.op == "init-exposed-snap-home" {
			return errors.New("boom")
		}
		return nil
	}

	s.settle(c)
	c.Assert(chg.Err(), Not(IsNil))
	c.Assert(chg.Status(), Equals, state.ErrorStatus)

	// only the ~/Snap was done and undone
	c.Assert(s.fakeBackend.ops.First("hide-snap-data"), IsNil)
	c.Assert(s.fakeBackend.ops.First("undo-hide-snap-data"), IsNil)

	expected := &dirs.SnapDirOptions{HiddenSnapDataDir: true}
	assertMigrationState(c, s.state, "snap-core18-to-core22", expected)
}

// assertMigrationState checks the migration status in the state and sequence
// file. Fails if no state or sequence file exist.
func assertMigrationState(c *C, st *state.State, snap string, expected *dirs.SnapDirOptions) {
	if expected == nil {
		expected = &dirs.SnapDirOptions{}
	}

	// check snap state has expected migration value
	var snapst snapstate.SnapState
	c.Assert(snapstate.Get(st, snap, &snapst), IsNil)
	c.Assert(snapst.MigratedHidden, Equals, expected.HiddenSnapDataDir)
	c.Assert(snapst.MigratedToExposedHome, Equals, expected.MigratedToExposedHome)

	assertMigrationInSeqFile(c, snap, expected)
}

func assertMigrationInSeqFile(c *C, snap string, expected *dirs.SnapDirOptions) {
	if expected == nil {
		expected = &dirs.SnapDirOptions{}
	}

	seqFilePath := filepath.Join(dirs.SnapSeqDir, snap+".json")
	file, err := os.Open(seqFilePath)
	c.Assert(err, IsNil)
	defer file.Close()

	data, err := io.ReadAll(file)
	c.Assert(err, IsNil)

	// check sequence file has expected migration value
	type seqData struct {
		MigratedHidden        bool `json:"migrated-hidden"`
		MigratedToExposedHome bool `json:"migrated-exposed-home"`
	}
	var d seqData
	c.Assert(json.Unmarshal(data, &d), IsNil)
	c.Assert(d.MigratedHidden, Equals, expected.HiddenSnapDataDir)
	c.Assert(d.MigratedToExposedHome, Equals, expected.MigratedToExposedHome)
}

func (s *snapmgrTestSuite) TestUndoInstallAfterDeletingRevisions(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	// 1 and 2 should be deleted, 3 is target revision and 4 is current
	seq := []*snap.SideInfo{
		{
			RealName: "some-snap",
			SnapID:   "some-snap-id",
			Revision: snap.R(1),
		},
		{
			RealName: "some-snap",
			SnapID:   "some-snap-id",
			Revision: snap.R(2),
		},
		{
			RealName: "some-snap",
			SnapID:   "some-snap-id",
			Revision: snap.R(3),
		},
		{
			RealName: "some-snap",
			SnapID:   "some-snap-id",
			Revision: snap.R(4),
		},
	}
	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos(seq),
		Current:  seq[len(seq)-1].Revision,
	})

	tr := config.NewTransaction(s.state)
	// remove the first two revisions so the old-candidate-index+1 (in undoLinkSnap) would be out of bounds if we didn't
	// account for discarded revisions
	c.Assert(tr.Set("core", "refresh.retain", 1), IsNil)
	tr.Commit()

	s.o.TaskRunner().AddHandler("fail", func(*state.Task, *tomb.Tomb) error {
		return errors.New("expected")
	}, nil)

	// install already stored revision
	ts, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Revision: seq[len(seq)-2].Revision}, s.user.ID, snapstate.Flags{NoReRefresh: true})
	c.Assert(err, IsNil)
	c.Assert(ts, NotNil)
	chg := s.state.NewChange("refresh", "")
	chg.AddAll(ts)

	// make update fail after removing old snaps
	failTask := s.state.NewTask("fail", "expected")
	disc := findLastTask(chg, "discard-snap")
	for _, lane := range disc.Lanes() {
		failTask.JoinLane(lane)
	}
	failTask.WaitFor(disc)
	chg.AddTask(failTask)

	s.settle(c)

	c.Assert(chg.Status(), Equals, state.ErrorStatus)

	var snapst snapstate.SnapState
	err = snapstate.Get(s.state, "some-snap", &snapst)
	c.Assert(err, IsNil)
	c.Assert(snapst.Sequence, DeepEquals, snapstatetest.NewSequenceFromSnapSideInfos(seq[2:]))

	linkTask := findLastTask(chg, "link-snap")
	c.Check(linkTask.Status(), Equals, state.UndoneStatus)

	var oldRevsBeforeCand []snap.Revision
	c.Assert(linkTask.Get("old-revs-before-cand", &oldRevsBeforeCand), IsNil)
	c.Assert(oldRevsBeforeCand, DeepEquals, []snap.Revision{seq[0].Revision, seq[1].Revision})
}

func findLastTask(chg *state.Change, kind string) *state.Task {
	var last *state.Task

	for _, task := range chg.Tasks() {
		if task.Kind() == kind {
			last = task
		}
	}

	return last
}

func (s *snapmgrTestSuite) TestUpdateBaseKernelSingleRebootHappy(c *C) {
	restore := release.MockOnClassic(false)
	defer restore()
	restore = snapstate.MockRevisionDate(nil)
	defer restore()

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

	var restartRequested []restart.RestartType
	_, err := restart.Manager(s.state, "boot-id-0", snapstatetest.MockRestartHandler(func(t restart.RestartType) {
		restartRequested = append(restartRequested, t)
	}))
	c.Assert(err, IsNil)

	restore = snapstatetest.MockDeviceModel(MakeModel(map[string]any{
		"kernel": "kernel",
		"base":   "core18",
	}))
	defer restore()

	siKernel := snap.SideInfo{
		RealName: "kernel",
		Revision: snap.R(7),
		SnapID:   "kernel-id",
	}
	siBase := snap.SideInfo{
		RealName: "core18",
		Revision: snap.R(7),
		SnapID:   "core18-snap-id",
	}
	for _, si := range []*snap.SideInfo{&siKernel, &siBase} {
		snaptest.MockSnap(c, fmt.Sprintf(`name: %s`, si.RealName), si)
		typ := "kernel"
		if si.RealName == "core18" {
			typ = "base"
		}
		snapstate.Set(s.state, si.RealName, &snapstate.SnapState{
			Active:          true,
			Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
			Current:         si.Revision,
			TrackingChannel: "latest/stable",
			SnapType:        typ,
		})
	}

	chg := s.state.NewChange("refresh", "refresh kernel and base")
	affected, tss, err := snapstate.UpdateMany(context.Background(), s.state,
		[]string{"kernel", "core18"}, nil, s.user.ID, &snapstate.Flags{})
	c.Assert(err, IsNil)
	c.Assert(affected, DeepEquals, []string{"core18", "kernel"})

	// Verify that correct dependencies have been set-up for single-reboot
	// which is a bit more tricky, as task-sets have been split up into pre-boot
	// things, and post-boot things.

	// Grab the correct task-sets
	var baseTs, kernelTs *state.TaskSet
	for _, ts := range tss {
		for _, t := range ts.Tasks() {
			snapsup, err := snapstate.TaskSnapSetup(t)
			if err != nil {
				continue
			}
			if snapsup.Type == snap.TypeKernel {
				kernelTs = ts
				break
			} else if snapsup.Type == snap.TypeBase {
				baseTs = ts
				break
			}
		}
		chg.AddAll(ts)
	}
	c.Assert(baseTs, NotNil)
	c.Assert(kernelTs, NotNil)

	// Grab the tasks we need to check dependencies between
	firstTaskOfKernel := firstTaskAfterLocalModifications(c, kernelTs)
	mountTaskOfKernel := findKindInTaskSet(kernelTs, "mount-snap")
	c.Assert(mountTaskOfKernel, NotNil)
	firstPostMountTaskOfKernel := firstTaskAfterMount(c, kernelTs)
	mountTaskOfBase := findKindInTaskSet(baseTs, "mount-snap")
	c.Assert(mountTaskOfBase, NotNil)
	beginTaskOfKernel, err := kernelTs.Edge(snapstate.BeginEdge)
	c.Assert(err, IsNil)
	linkTaskOfKernel, err := kernelTs.Edge(snapstate.MaybeRebootEdge)
	c.Assert(err, IsNil)
	acTaskOfKernel, err := kernelTs.Edge(snapstate.MaybeRebootWaitEdge)
	c.Assert(err, IsNil)
	linkTaskOfBase, err := baseTs.Edge(snapstate.MaybeRebootEdge)
	c.Assert(err, IsNil)
	acTaskOfBase, err := baseTs.Edge(snapstate.MaybeRebootWaitEdge)
	c.Assert(err, IsNil)
	lastTaskOfBase, err := baseTs.Edge(snapstate.EndEdge)
	c.Assert(err, IsNil)

	// Things that must be correct:
	// - first local modification task of kernel must be its prerequisites sync task
	// - the kernel mount task must run after the base mount
	c.Check(firstTaskOfKernel.Kind(), Equals, "prerequisites")
	c.Check(mountTaskOfKernel.WaitTasks(), testutil.Contains, mountTaskOfBase)
	// - the first post-mount task of kernel must depend on the base link
	c.Check(firstPostMountTaskOfKernel.WaitTasks(), testutil.Contains, linkTaskOfBase)
	// - prerequisites/download should not be serialized behind base link
	c.Check(beginTaskOfKernel.WaitTasks(), Not(testutil.Contains), linkTaskOfBase)
	// - "auto-connect" (MaybeRebootWaitEdge) of base must depend on "link-snap" of kernel (MaybeRebootEdge)
	c.Check(acTaskOfBase.WaitTasks(), testutil.Contains, linkTaskOfKernel)
	// - "auto-connect" (MaybeRebootWaitEdge) of kernel must depend on the last task of base (EndEdge)
	c.Check(acTaskOfKernel.WaitTasks(), testutil.Contains, lastTaskOfBase)

	// Core18 and kernel should be in the same transactional lane
	c.Check(taskSetsShareLane(baseTs, kernelTs), Equals, true)

	// Manually verify the lanes of the initial task for the 4 task-sets
	c.Check(kernelTs.Tasks()[0].Lanes(), DeepEquals, []int{1, 2})
	c.Check(baseTs.Tasks()[0].Lanes(), DeepEquals, []int{2, 1})

	// have fake backend indicate a need to reboot for both snaps
	s.fakeBackend.linkSnapMaybeReboot = true
	s.fakeBackend.linkSnapRebootFor = map[string]bool{
		"kernel": true,
		"core18": true,
	}

	s.settle(c)

	// mock restart for the 'link-snap' step and run change to
	// completion.
	s.mockRestartAndSettle(c, chg)

	c.Check(chg.Status(), Equals, state.DoneStatus)
	// a single system restart was requested
	c.Check(restartRequested, DeepEquals, []restart.RestartType{
		restart.RestartSystem,
	})

	for _, name := range []string{"kernel", "core18"} {
		snapID := "kernel-id"
		if name == "core18" {
			snapID = "core18-snap-id"
		}
		var snapst snapstate.SnapState
		err = snapstate.Get(s.state, name, &snapst)
		c.Assert(err, IsNil)

		c.Assert(snapst.Active, Equals, true)
		c.Assert(snapst.Sequence.Revisions, HasLen, 2)
		c.Assert(snapst.Sequence.Revisions[0], DeepEquals, sequence.NewRevisionSideState(&snap.SideInfo{
			RealName: name,
			SnapID:   snapID,
			Channel:  "",
			Revision: snap.R(7),
		}, nil))
		c.Assert(snapst.Sequence.Revisions[1], DeepEquals, sequence.NewRevisionSideState(&snap.SideInfo{
			RealName: name,
			Channel:  "latest/stable",
			SnapID:   snapID,
			Revision: snap.R(11),
		}, nil))
	}

	// ops come in semi random order, but we know that link and auto-connect
	// operations will be done in a specific order,
	ops := make([]string, 0, 8)
	for _, op := range s.fakeBackend.ops {
		if op.op == "link-snap" {
			split := strings.Split(op.path, "/")
			c.Assert(len(split) > 2, Equals, true)
			ops = append(ops, filepath.Join(split[len(split)-2:]...))
		} else if op.op == "cleanup-trash" {
			ops = append(ops, fmt.Sprintf("%s-%s", op.op, op.name))
		} else if strings.HasPrefix(op.op, "auto-connect:") || strings.HasPrefix(op.op, "setup-profiles:") {
			ops = append(ops, fmt.Sprintf("%s-%s/%s", op.op, op.name, op.revno))
		}
	}
	// With setup-profiles moved after auto-connect, operations may interleave
	// per-snap. Keep the strict ordering checks per operation type.
	linkOps := make([]string, 0, 2)
	autoConnectOps := make([]string, 0, 2)
	cleanupOps := make([]string, 0, 2)
	for _, op := range ops {
		switch {
		case op == "kernel/11" || op == "core18/11":
			linkOps = append(linkOps, op)
		case strings.HasPrefix(op, "auto-connect:"):
			autoConnectOps = append(autoConnectOps, op)
		case strings.HasPrefix(op, "cleanup-trash-"):
			cleanupOps = append(cleanupOps, op)
		}
	}
	c.Assert(linkOps, HasLen, 2)
	c.Assert(autoConnectOps, HasLen, 2)
	c.Assert(cleanupOps, HasLen, 2)
	c.Check(linkOps, testutil.DeepUnsortedMatches, []string{"kernel/11", "core18/11"})
	c.Check(autoConnectOps, DeepEquals, []string{"auto-connect:Doing-core18/11", "auto-connect:Doing-kernel/11"})
	c.Check(cleanupOps, testutil.DeepUnsortedMatches, []string{"cleanup-trash-core18", "cleanup-trash-kernel"})
}

func (s *snapmgrTestSuite) TestUpdateBaseKernelAndSnapdSingleRebootHappy(c *C) {
	restore := release.MockOnClassic(false)
	defer restore()
	restore = snapstate.MockRevisionDate(nil)
	defer restore()

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

	var restartRequested []restart.RestartType
	_, err := restart.Manager(s.state, "boot-id-0", snapstatetest.MockRestartHandler(func(t restart.RestartType) {
		restartRequested = append(restartRequested, t)
	}))
	c.Assert(err, IsNil)

	restore = snapstatetest.MockDeviceModel(MakeModel(map[string]any{
		"kernel": "kernel",
		"base":   "core18",
	}))
	defer restore()

	snaps := []struct {
		name string
		typ  string
		id   string
	}{
		{name: "snapd", typ: "snapd", id: "snapd-snap-id"},
		{name: "kernel", typ: "kernel", id: "kernel-id"},
		{name: "core18", typ: "base", id: "core18-snap-id"},
	}

	for _, sn := range snaps {
		si := &snap.SideInfo{
			RealName: sn.name,
			Revision: snap.R(7),
			SnapID:   sn.id,
		}
		snaptest.MockSnap(c, fmt.Sprintf("name: %s\nversion: 1.2.3\ntype: %s", sn.name, sn.typ), si)
		snapstate.Set(s.state, sn.name, &snapstate.SnapState{
			Active:          true,
			Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
			Current:         si.Revision,
			TrackingChannel: "latest/stable",
			SnapType:        sn.typ,
		})
	}

	chg := s.state.NewChange("refresh", "refresh snapd, kernel and base")
	affected, tss, err := snapstate.UpdateMany(context.Background(), s.state,
		[]string{"snapd", "kernel", "core18"}, nil, s.user.ID, &snapstate.Flags{})
	c.Assert(err, IsNil)
	c.Assert(affected, testutil.DeepUnsortedMatches, []string{"snapd", "kernel", "core18"})

	var baseTs, kernelTs, snapdTs *state.TaskSet
	for _, ts := range tss {
		for _, t := range ts.Tasks() {
			snapsup, err := snapstate.TaskSnapSetup(t)
			if err != nil {
				continue
			}
			switch snapsup.Type {
			case snap.TypeSnapd:
				snapdTs = ts
			case snap.TypeKernel:
				kernelTs = ts
			case snap.TypeBase:
				baseTs = ts
			}
		}
		chg.AddAll(ts)
	}
	c.Assert(baseTs, NotNil)
	c.Assert(kernelTs, NotNil)
	c.Assert(snapdTs, NotNil)

	beginTaskOfBase, err := baseTs.Edge(snapstate.BeginEdge)
	c.Assert(err, IsNil)
	firstTaskOfKernel := firstTaskAfterLocalModifications(c, kernelTs)
	mountTaskOfKernel := findKindInTaskSet(kernelTs, "mount-snap")
	c.Assert(mountTaskOfKernel, NotNil)
	firstPostMountTaskOfKernel := firstTaskAfterMount(c, kernelTs)
	mountTaskOfBase := findKindInTaskSet(baseTs, "mount-snap")
	c.Assert(mountTaskOfBase, NotNil)
	beginTaskOfKernel, err := kernelTs.Edge(snapstate.BeginEdge)
	c.Assert(err, IsNil)
	linkTaskOfBase, err := baseTs.Edge(snapstate.MaybeRebootEdge)
	c.Assert(err, IsNil)
	acTaskOfBase, err := baseTs.Edge(snapstate.MaybeRebootWaitEdge)
	c.Assert(err, IsNil)
	lastTaskOfBase, err := baseTs.Edge(snapstate.EndEdge)
	c.Assert(err, IsNil)
	linkTaskOfKernel, err := kernelTs.Edge(snapstate.MaybeRebootEdge)
	c.Assert(err, IsNil)
	acTaskOfKernel, err := kernelTs.Edge(snapstate.MaybeRebootWaitEdge)
	c.Assert(err, IsNil)
	snapdEndTask, err := snapdTs.Edge(snapstate.EndEdge)
	c.Assert(err, IsNil)

	c.Check(beginTaskOfBase.WaitTasks(), testutil.Contains, snapdEndTask)
	c.Check(beginTaskOfKernel.WaitTasks(), testutil.Contains, snapdEndTask)
	c.Check(firstTaskOfKernel.Kind(), Equals, "prerequisites")
	c.Check(mountTaskOfKernel.WaitTasks(), testutil.Contains, mountTaskOfBase)
	c.Check(firstPostMountTaskOfKernel.WaitTasks(), testutil.Contains, linkTaskOfBase)
	c.Check(beginTaskOfKernel.WaitTasks(), Not(testutil.Contains), linkTaskOfBase)
	c.Check(acTaskOfBase.WaitTasks(), testutil.Contains, linkTaskOfKernel)
	c.Check(acTaskOfKernel.WaitTasks(), testutil.Contains, lastTaskOfBase)

	// core18 and kernel should be in the same transactional lane.
	c.Check(taskSetsShareLane(baseTs, kernelTs), Equals, true)

	// snapd should not be in the single-reboot transactional lane.
	c.Check(taskSetsShareLane(snapdTs, baseTs), Equals, false)
	c.Check(taskSetsShareLane(snapdTs, kernelTs), Equals, false)

	// have fake backend indicate a need to reboot for base and kernel
	s.fakeBackend.linkSnapMaybeReboot = true
	s.fakeBackend.linkSnapRebootFor = map[string]bool{
		"kernel": true,
		"core18": true,
	}

	s.settle(c)

	// mock restart for the 'link-snap' step and run change to completion.
	s.mockRestartAndSettle(c, chg)

	c.Check(chg.Status(), Equals, state.DoneStatus)
	c.Check(restartRequested, DeepEquals, []restart.RestartType{
		restart.RestartDaemon,
		restart.RestartSystem,
	})

	for _, name := range []string{"snapd", "kernel", "core18"} {
		snapID := "snapd-snap-id"
		switch name {
		case "kernel":
			snapID = "kernel-id"
		case "core18":
			snapID = "core18-snap-id"
		}

		var snapst snapstate.SnapState
		err = snapstate.Get(s.state, name, &snapst)
		c.Assert(err, IsNil)

		c.Assert(snapst.Active, Equals, true)
		c.Assert(snapst.Sequence.Revisions, HasLen, 2)
		c.Assert(snapst.Sequence.Revisions[0], DeepEquals, sequence.NewRevisionSideState(&snap.SideInfo{
			RealName: name,
			SnapID:   snapID,
			Channel:  "",
			Revision: snap.R(7),
		}, nil))
		c.Assert(snapst.Sequence.Revisions[1], DeepEquals, sequence.NewRevisionSideState(&snap.SideInfo{
			RealName: name,
			SnapID:   snapID,
			Channel:  "latest/stable",
			Revision: snap.R(11),
		}, nil))
	}
}

func (s *snapmgrTestSuite) TestUpdateGadgetKernelSingleRebootHappy(c *C) {
	restore := release.MockOnClassic(false)
	defer restore()
	restore = snapstate.MockRevisionDate(nil)
	defer restore()

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

	// Handle gadget update tasks to avoid making tests unnecessarily complex
	s.o.TaskRunner().AddHandler("update-gadget-assets",
		func(task *state.Task, tomb *tomb.Tomb) error {
			task.State().Lock()
			defer task.State().Unlock()
			chg := task.Change()
			chg.Set("gadget-restart-required", true)
			return nil
		},
		func(task *state.Task, tomb *tomb.Tomb) error { return nil })

	s.o.TaskRunner().AddHandler("update-gadget-cmdline",
		func(task *state.Task, tomb *tomb.Tomb) error { return nil },
		func(task *state.Task, tomb *tomb.Tomb) error { return nil })

	var restartRequested []restart.RestartType
	_, err := restart.Manager(s.state, "boot-id-0", snapstatetest.MockRestartHandler(func(t restart.RestartType) {
		restartRequested = append(restartRequested, t)
	}))
	c.Assert(err, IsNil)

	restore = snapstatetest.MockDeviceModel(ModelWithBase("core18"))
	defer restore()

	siKernel := snap.SideInfo{
		RealName: "kernel",
		Revision: snap.R(7),
		SnapID:   "kernel-id",
	}
	siGadget := snap.SideInfo{
		RealName: "gadget",
		Revision: snap.R(7),
		SnapID:   "gadget-core18-id",
	}
	for _, si := range []*snap.SideInfo{&siKernel, &siGadget} {
		snaptest.MockSnap(c, fmt.Sprintf(`name: %s`, si.RealName), si)
		typ := "kernel"
		if si.RealName == "gadget" {
			typ = "gadget"
		}
		snapstate.Set(s.state, si.RealName, &snapstate.SnapState{
			Active:          true,
			Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
			Current:         si.Revision,
			TrackingChannel: "latest/stable",
			SnapType:        typ,
		})
	}

	chg := s.state.NewChange("refresh", "refresh kernel and gadget")
	affected, tss, err := snapstate.UpdateMany(context.Background(), s.state,
		[]string{"gadget", "kernel"}, nil, s.user.ID, &snapstate.Flags{})
	c.Assert(err, IsNil)
	c.Assert(affected, DeepEquals, []string{"gadget", "kernel"})

	// Verify that correct dependencies have been set-up for single-reboot
	// which is a bit more tricky, as task-sets have been split up into pre-boot
	// things, and post-boot things.

	// Grab the correct task-sets
	var gadgetTs, kernelTs *state.TaskSet
	for _, ts := range tss {
		for _, t := range ts.Tasks() {
			snapsup, err := snapstate.TaskSnapSetup(t)
			if err != nil {
				continue
			}
			if snapsup.Type == snap.TypeKernel {
				kernelTs = ts
				break
			} else if snapsup.Type == snap.TypeGadget {
				gadgetTs = ts
				break
			}
		}
		chg.AddAll(ts)
	}
	c.Assert(gadgetTs, NotNil)
	c.Assert(kernelTs, NotNil)

	// Grab the tasks we need to check dependencies between
	firstTaskOfKernel := firstTaskAfterLocalModifications(c, kernelTs)
	mountTaskOfKernel := findKindInTaskSet(kernelTs, "mount-snap")
	c.Assert(mountTaskOfKernel, NotNil)
	firstPostMountTaskOfKernel := firstTaskAfterMount(c, kernelTs)
	mountTaskOfGadget := findKindInTaskSet(gadgetTs, "mount-snap")
	c.Assert(mountTaskOfGadget, NotNil)
	linkTaskOfKernel, err := kernelTs.Edge(snapstate.MaybeRebootEdge)
	c.Assert(err, IsNil)
	acTaskOfKernel, err := kernelTs.Edge(snapstate.MaybeRebootWaitEdge)
	c.Assert(err, IsNil)
	linkTaskOfGadget, err := gadgetTs.Edge(snapstate.MaybeRebootEdge)
	c.Assert(err, IsNil)
	acTaskOfGadget, err := gadgetTs.Edge(snapstate.MaybeRebootWaitEdge)
	c.Assert(err, IsNil)
	lastTaskOfGadget, err := gadgetTs.Edge(snapstate.EndEdge)
	c.Assert(err, IsNil)

	// Things that must be correct:
	// - first local modification task of kernel must be its prerequisites sync task
	// - the kernel mount task must run after the gadget mount
	c.Check(firstTaskOfKernel.Kind(), Equals, "prerequisites")
	c.Check(mountTaskOfKernel.WaitTasks(), testutil.Contains, mountTaskOfGadget)
	// - the first post-mount task of kernel must depend on the gadget link
	c.Check(firstPostMountTaskOfKernel.WaitTasks(), testutil.Contains, linkTaskOfGadget)
	// - "auto-connect" (MaybeRebootWaitEdge) of gadget must depend on "link-snap" of kernel (MaybeRebootEdge)
	c.Check(acTaskOfGadget.WaitTasks(), testutil.Contains, linkTaskOfKernel)
	// - "auto-connect" (MaybeRebootWaitEdge) of kernel must depend on the last task of gadget (EndEdge)
	c.Check(acTaskOfKernel.WaitTasks(), testutil.Contains, lastTaskOfGadget)

	// Gadget and kernel should be in the same transactional lane
	c.Check(taskSetsShareLane(gadgetTs, kernelTs), Equals, true)

	// Manually verify the lanes of the initial task for the 4 task-sets
	c.Check(kernelTs.Tasks()[0].Lanes(), DeepEquals, []int{1, 2})
	c.Check(gadgetTs.Tasks()[0].Lanes(), DeepEquals, []int{2, 1})

	// have fake backend indicate a need to reboot for both snaps
	s.fakeBackend.linkSnapMaybeReboot = true
	s.fakeBackend.linkSnapRebootFor = map[string]bool{
		"kernel": true,
		"gadget": true,
	}

	s.settle(c)

	// mock restart for the 'link-snap' step and run change to
	// completion.
	s.mockRestartAndSettle(c, chg)

	c.Check(chg.Status(), Equals, state.DoneStatus)
	// a single system restart was requested
	c.Check(restartRequested, DeepEquals, []restart.RestartType{
		restart.RestartSystem,
	})

	for _, name := range []string{"kernel", "gadget"} {
		snapID := "kernel-id"
		if name == "gadget" {
			snapID = "gadget-core18-id"
		}
		var snapst snapstate.SnapState
		err = snapstate.Get(s.state, name, &snapst)
		c.Assert(err, IsNil)

		c.Assert(snapst.Active, Equals, true)
		c.Assert(snapst.Sequence.Revisions, HasLen, 2)
		c.Assert(snapst.Sequence.Revisions[0], DeepEquals, sequence.NewRevisionSideState(&snap.SideInfo{
			RealName: name,
			SnapID:   snapID,
			Channel:  "",
			Revision: snap.R(7),
		}, nil))
		c.Assert(snapst.Sequence.Revisions[1], DeepEquals, sequence.NewRevisionSideState(&snap.SideInfo{
			RealName: name,
			Channel:  "latest/stable",
			SnapID:   snapID,
			Revision: snap.R(11),
		}, nil))
	}

	// ops come in semi random order, but we know that link and auto-connect
	// operations will be done in a specific order,
	ops := make([]string, 0, 8)
	for _, op := range s.fakeBackend.ops {
		if op.op == "link-snap" {
			split := strings.Split(op.path, "/")
			c.Assert(len(split) > 2, Equals, true)
			ops = append(ops, filepath.Join(split[len(split)-2:]...))
		} else if op.op == "cleanup-trash" {
			ops = append(ops, fmt.Sprintf("%s-%s", op.op, op.name))
		} else if strings.HasPrefix(op.op, "auto-connect:") || strings.HasPrefix(op.op, "setup-profiles:") {
			ops = append(ops, fmt.Sprintf("%s-%s/%s", op.op, op.name, op.revno))
		}
	}
	// With setup-profiles moved after auto-connect, operations may interleave
	// per-snap. Keep the strict ordering checks per operation type.
	linkOps := make([]string, 0, 2)
	autoConnectOps := make([]string, 0, 2)
	cleanupOps := make([]string, 0, 2)
	for _, op := range ops {
		switch {
		case op == "kernel/11" || op == "gadget/11":
			linkOps = append(linkOps, op)
		case strings.HasPrefix(op, "auto-connect:"):
			autoConnectOps = append(autoConnectOps, op)
		case strings.HasPrefix(op, "cleanup-trash-"):
			cleanupOps = append(cleanupOps, op)
		}
	}
	c.Assert(linkOps, HasLen, 2)
	c.Assert(autoConnectOps, HasLen, 2)
	c.Assert(cleanupOps, HasLen, 2)
	c.Check(linkOps, testutil.DeepUnsortedMatches, []string{"kernel/11", "gadget/11"})
	c.Check(autoConnectOps, DeepEquals, []string{"auto-connect:Doing-gadget/11", "auto-connect:Doing-kernel/11"})
	c.Check(cleanupOps, testutil.DeepUnsortedMatches, []string{"cleanup-trash-gadget", "cleanup-trash-kernel"})
}

func (s *snapmgrTestSuite) TestUpdateBaseGadgetSingleRebootHappy(c *C) {
	restore := release.MockOnClassic(false)
	defer restore()
	restore = snapstate.MockRevisionDate(nil)
	defer restore()

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

	// Handle gadget update tasks to avoid making tests unnecessarily complex
	s.o.TaskRunner().AddHandler("update-gadget-assets",
		func(task *state.Task, tomb *tomb.Tomb) error {
			task.State().Lock()
			defer task.State().Unlock()
			chg := task.Change()
			chg.Set("gadget-restart-required", true)
			return nil
		},
		func(task *state.Task, tomb *tomb.Tomb) error { return nil })

	s.o.TaskRunner().AddHandler("update-gadget-cmdline",
		func(task *state.Task, tomb *tomb.Tomb) error { return nil },
		func(task *state.Task, tomb *tomb.Tomb) error { return nil })

	var restartRequested []restart.RestartType
	_, err := restart.Manager(s.state, "boot-id-0", snapstatetest.MockRestartHandler(func(t restart.RestartType) {
		restartRequested = append(restartRequested, t)
	}))
	c.Assert(err, IsNil)

	restore = snapstatetest.MockDeviceModel(ModelWithBase("core18"))
	defer restore()

	siBase := snap.SideInfo{
		RealName: "core18",
		Revision: snap.R(7),
		SnapID:   "core18-snap-id",
	}
	siGadget := snap.SideInfo{
		RealName: "gadget",
		Revision: snap.R(7),
		SnapID:   "gadget-core18-id",
	}
	for _, si := range []*snap.SideInfo{&siBase, &siGadget} {
		snaptest.MockSnap(c, fmt.Sprintf(`name: %s`, si.RealName), si)
		typ := "kernel"
		if si.RealName == "core18" {
			typ = "base"
		}
		snapstate.Set(s.state, si.RealName, &snapstate.SnapState{
			Active:          true,
			Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
			Current:         si.Revision,
			TrackingChannel: "latest/stable",
			SnapType:        typ,
		})
	}

	chg := s.state.NewChange("refresh", "refresh base and gadget")
	affected, tss, err := snapstate.UpdateMany(context.Background(), s.state,
		[]string{"gadget", "core18"}, nil, s.user.ID, &snapstate.Flags{})
	c.Assert(err, IsNil)
	c.Check(affected, DeepEquals, []string{"core18", "gadget"})

	// Verify that correct dependencies have been set-up for single-reboot
	// which is a bit more tricky, as task-sets have been split up into pre-boot
	// things, and post-boot things.

	// Grab the correct task-sets
	var gadgetTs, baseTs *state.TaskSet
	for _, ts := range tss {
		for _, t := range ts.Tasks() {
			snapsup, err := snapstate.TaskSnapSetup(t)
			if err != nil {
				continue
			}
			if snapsup.Type == snap.TypeBase {
				baseTs = ts
				break
			} else if snapsup.Type == snap.TypeGadget {
				gadgetTs = ts
				break
			}
		}
		chg.AddAll(ts)
	}
	c.Assert(gadgetTs, NotNil)
	c.Assert(baseTs, NotNil)

	// Grab the tasks we need to check dependencies between
	linkTaskOfBase, err := baseTs.Edge(snapstate.MaybeRebootEdge)
	c.Assert(err, IsNil)
	acTaskOfBase, err := baseTs.Edge(snapstate.MaybeRebootWaitEdge)
	c.Assert(err, IsNil)
	lastTaskOfBase, err := baseTs.Edge(snapstate.EndEdge)
	c.Assert(err, IsNil)
	firstTaskOfGadget := firstTaskAfterLocalModifications(c, gadgetTs)
	mountTaskOfGadget := findKindInTaskSet(gadgetTs, "mount-snap")
	c.Assert(mountTaskOfGadget, NotNil)
	firstPostMountTaskOfGadget := firstTaskAfterMount(c, gadgetTs)
	mountTaskOfBase := findKindInTaskSet(baseTs, "mount-snap")
	c.Assert(mountTaskOfBase, NotNil)
	linkTaskOfGadget, err := gadgetTs.Edge(snapstate.MaybeRebootEdge)
	c.Assert(err, IsNil)
	acTaskOfGadget, err := gadgetTs.Edge(snapstate.MaybeRebootWaitEdge)
	c.Assert(err, IsNil)

	// - first local modification task of gadget must be its prerequisites sync task
	// - the gadget mount task must run after the base mount
	c.Check(firstTaskOfGadget.Kind(), Equals, "prerequisites")
	c.Check(mountTaskOfGadget.WaitTasks(), testutil.Contains, mountTaskOfBase)
	// - the first post-mount task of gadget must depend on the base link
	c.Check(firstPostMountTaskOfGadget.WaitTasks(), testutil.Contains, linkTaskOfBase)
	// - "auto-connect" (MaybeRebootWaitEdge) of base must depend on "link-snap" of gadget (MaybeRebootEdge)
	c.Check(acTaskOfBase.WaitTasks(), testutil.Contains, linkTaskOfGadget)
	// - "auto-connect" (MaybeRebootWaitEdge) of gadget must depend on the last task of base (EndEdge)
	c.Check(acTaskOfGadget.WaitTasks(), testutil.Contains, lastTaskOfBase)

	// Gadget and base should be in the same transactional lane
	c.Check(taskSetsShareLane(gadgetTs, baseTs), Equals, true)

	// Manually verify the lanes of the initial task for the 4 task-sets
	c.Check(baseTs.Tasks()[0].Lanes(), DeepEquals, []int{1, 2})
	c.Check(gadgetTs.Tasks()[0].Lanes(), DeepEquals, []int{2, 1})

	// have fake backend indicate a need to reboot for both snaps
	s.fakeBackend.linkSnapMaybeReboot = true
	s.fakeBackend.linkSnapRebootFor = map[string]bool{
		"core18": true,
		"gadget": true,
	}

	s.settle(c)

	// mock restart for the 'link-snap' step and run change to
	// completion.
	s.mockRestartAndSettle(c, chg)

	c.Check(chg.Status(), Equals, state.DoneStatus)
	// a single system restart was requested
	c.Check(restartRequested, DeepEquals, []restart.RestartType{
		restart.RestartSystem,
	})

	for _, name := range []string{"core18", "gadget"} {
		snapID := "core18-snap-id"
		if name == "gadget" {
			snapID = "gadget-core18-id"
		}
		var snapst snapstate.SnapState
		err = snapstate.Get(s.state, name, &snapst)
		c.Assert(err, IsNil)

		c.Assert(snapst.Active, Equals, true)
		c.Assert(snapst.Sequence.Revisions, HasLen, 2)
		c.Assert(snapst.Sequence.Revisions[0], DeepEquals, sequence.NewRevisionSideState(&snap.SideInfo{
			RealName: name,
			SnapID:   snapID,
			Channel:  "",
			Revision: snap.R(7),
		}, nil))
		c.Assert(snapst.Sequence.Revisions[1], DeepEquals, sequence.NewRevisionSideState(&snap.SideInfo{
			RealName: name,
			Channel:  "latest/stable",
			SnapID:   snapID,
			Revision: snap.R(11),
		}, nil))
	}

	// ops come in semi random order, but we know that link and auto-connect
	// operations will be done in a specific order,
	ops := make([]string, 0, 8)
	for _, op := range s.fakeBackend.ops {
		if op.op == "link-snap" {
			split := strings.Split(op.path, "/")
			c.Assert(len(split) > 2, Equals, true)
			ops = append(ops, filepath.Join(split[len(split)-2:]...))
		} else if op.op == "cleanup-trash" {
			ops = append(ops, fmt.Sprintf("%s-%s", op.op, op.name))
		} else if strings.HasPrefix(op.op, "auto-connect:") || strings.HasPrefix(op.op, "setup-profiles:") {
			ops = append(ops, fmt.Sprintf("%s-%s/%s", op.op, op.name, op.revno))
		}
	}
	// With setup-profiles moved after auto-connect, operations may interleave
	// per-snap. Keep the strict ordering checks per operation type.
	linkOps := make([]string, 0, 2)
	autoConnectOps := make([]string, 0, 2)
	cleanupOps := make([]string, 0, 2)
	for _, op := range ops {
		switch {
		case op == "core18/11" || op == "gadget/11":
			linkOps = append(linkOps, op)
		case strings.HasPrefix(op, "auto-connect:"):
			autoConnectOps = append(autoConnectOps, op)
		case strings.HasPrefix(op, "cleanup-trash-"):
			cleanupOps = append(cleanupOps, op)
		}
	}
	c.Assert(linkOps, HasLen, 2)
	c.Assert(autoConnectOps, HasLen, 2)
	c.Assert(cleanupOps, HasLen, 2)
	c.Check(linkOps, testutil.DeepUnsortedMatches, []string{"core18/11", "gadget/11"})
	c.Check(autoConnectOps, DeepEquals, []string{"auto-connect:Doing-core18/11", "auto-connect:Doing-gadget/11"})
	c.Check(cleanupOps, testutil.DeepUnsortedMatches, []string{"cleanup-trash-gadget", "cleanup-trash-core18"})
}

func (s *snapmgrTestSuite) TestUpdateBaseKernelSingleRebootWithCannotRebootSetHappy(c *C) {
	// Verify the single-reboot still works when using "cannot-reboot" variable, to maintain
	// backwards compatibility with the previous logic.
	restore := release.MockOnClassic(false)
	defer restore()
	restore = snapstate.MockRevisionDate(nil)
	defer restore()

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

	var restartRequested []restart.RestartType
	_, err := restart.Manager(s.state, "boot-id-0", snapstatetest.MockRestartHandler(func(t restart.RestartType) {
		restartRequested = append(restartRequested, t)
	}))
	c.Assert(err, IsNil)

	restore = snapstatetest.MockDeviceModel(MakeModel(map[string]any{
		"kernel": "kernel",
		"base":   "core18",
	}))
	defer restore()

	siKernel := snap.SideInfo{
		RealName: "kernel",
		Revision: snap.R(7),
		SnapID:   "kernel-id",
	}
	siBase := snap.SideInfo{
		RealName: "core18",
		Revision: snap.R(7),
		SnapID:   "core18-snap-id",
	}
	for _, si := range []*snap.SideInfo{&siKernel, &siBase} {
		snaptest.MockSnap(c, fmt.Sprintf(`name: %s`, si.RealName), si)
		typ := "kernel"
		if si.RealName == "core18" {
			typ = "base"
		}
		snapstate.Set(s.state, si.RealName, &snapstate.SnapState{
			Active:          true,
			Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
			Current:         si.Revision,
			TrackingChannel: "latest/stable",
			SnapType:        typ,
		})
	}

	chg := s.state.NewChange("refresh", "refresh kernel and base")
	affected, tss, err := snapstate.UpdateMany(context.Background(), s.state,
		[]string{"kernel", "core18"}, nil, s.user.ID, &snapstate.Flags{})
	c.Assert(err, IsNil)
	c.Assert(affected, DeepEquals, []string{"core18", "kernel"})

	// Get the link-snap task of base, and set "cannot-reboot"
	var linkSnapOfBase *state.Task
	for _, ts := range tss {
		chg.AddAll(ts)

		for _, t := range ts.Tasks() {
			if t.Kind() != "link-snap" {
				continue
			}

			snapsup, err := snapstate.TaskSnapSetup(t)
			if err != nil {
				continue
			}
			if snapsup.Type == snap.TypeBase {
				linkSnapOfBase = t
				break
			}
		}
	}
	c.Assert(linkSnapOfBase, NotNil)

	// Fake an older snapd having set this in a previous change
	linkSnapOfBase.Set("cannot-reboot", true)

	// have fake backend indicate a need to reboot for both snaps
	s.fakeBackend.linkSnapMaybeReboot = true
	s.fakeBackend.linkSnapRebootFor = map[string]bool{
		"kernel": true,
		"core18": true,
	}

	s.settle(c)

	// mock restart for the 'link-snap' step and run change to
	// completion.
	s.mockRestartAndSettle(c, chg)

	c.Check(chg.Status(), Equals, state.DoneStatus)
	// a single system restart was requested
	c.Check(restartRequested, DeepEquals, []restart.RestartType{
		restart.RestartSystem,
	})
	// verify that the log message appeared in the link-snap task
	c.Check(linkSnapOfBase.Log(), HasLen, 1)
	c.Check(linkSnapOfBase.Log()[0], Matches, `.* reboot postponed to later tasks`)
}

func (s *snapmgrTestSuite) TestUpdateBaseKernelSingleRebootUnsupportedWithCoreHappy(c *C) {
	restore := release.MockOnClassic(false)
	defer restore()
	restore = snapstate.MockRevisionDate(nil)
	defer restore()

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

	var restartRequested []restart.RestartType
	_, err := restart.Manager(s.state, "boot-id-0", snapstatetest.MockRestartHandler(func(t restart.RestartType) {
		restartRequested = append(restartRequested, t)
	}))
	c.Assert(err, IsNil)

	restore = snapstatetest.MockDeviceModel(DefaultModel())
	defer restore()

	siKernel := snap.SideInfo{
		RealName: "kernel",
		Revision: snap.R(7),
		SnapID:   "kernel-id",
	}
	siCore := snap.SideInfo{
		RealName: "core",
		Revision: snap.R(7),
		SnapID:   "core-snap-id",
	}
	for _, si := range []*snap.SideInfo{&siKernel, &siCore} {
		snaptest.MockSnap(c, fmt.Sprintf(`name: %s`, si.RealName), si)
		typ := "kernel"
		if si.RealName == "core18" {
			typ = "base"
		}
		snapstate.Set(s.state, si.RealName, &snapstate.SnapState{
			Active:          true,
			Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
			Current:         si.Revision,
			TrackingChannel: "latest/stable",
			SnapType:        typ,
		})
	}

	chg := s.state.NewChange("refresh", "refresh kernel and base")
	affected, tss, err := snapstate.UpdateMany(context.Background(), s.state,
		[]string{"kernel", "core"}, nil, s.user.ID, &snapstate.Flags{})
	c.Assert(err, IsNil)
	c.Assert(affected, DeepEquals, []string{"core", "kernel"})
	var kernelTs, coreTs *state.TaskSet
	for _, ts := range tss {
		chg.AddAll(ts)
		for _, tsk := range ts.Tasks() {
			switch tsk.Kind() {
			// setup-profiles should appear right before link-snap
			case "link-snap", "auto-connect", "setup-profiles", "set-auto-aliases":
				snapsup, err := snapstate.TaskSnapSetup(tsk)
				c.Assert(err, IsNil)
				if tsk.Kind() == "link-snap" {
					opts := 0
					verifyUpdateTasks(c, snapsup.Type, opts, 0, ts)
					if snapsup.Type == snap.TypeOS {
						coreTs = ts
					} else if snapsup.Type == snap.TypeKernel {
						kernelTs = ts
					}
				}
			}
		}
	}

	// Core should come first, and it's "prerequisite" task should have no
	// dependencies. The first task of kernel should depend on the last task
	// of core. Single-reboot is not supported, so we expect them to run in serial.
	firstTaskOfBase, err := coreTs.Edge(snapstate.BeginEdge)
	c.Assert(err, IsNil)
	firstTaskOfKernel, err := kernelTs.Edge(snapstate.BeginEdge)
	c.Assert(err, IsNil)
	lastTaskOfBase, err := coreTs.Edge(snapstate.EndEdge)
	c.Assert(err, IsNil)
	c.Check(firstTaskOfBase.WaitTasks(), HasLen, 0)
	c.Check(firstTaskOfKernel.WaitTasks(), testutil.Contains, lastTaskOfBase)

	// Core and kernel should not be in the same transactional lane, as this
	// is behaviour we want to have on UC16
	c.Check(taskSetsShareLane(coreTs, kernelTs), Equals, false)

	// have fake backend indicate a need to reboot for both snaps
	s.fakeBackend.linkSnapMaybeReboot = true
	s.fakeBackend.linkSnapRebootFor = map[string]bool{
		"kernel": true,
		"core":   true,
	}

	s.settle(c)

	// first 'auto-connect' restart here
	s.mockRestartAndSettle(c, chg)

	// second 'auto-connect' restart here
	s.mockRestartAndSettle(c, chg)

	c.Check(chg.Status(), Equals, state.DoneStatus)
	// when updating both kernel that uses core as base, and "core" we have two reboots
	c.Check(restartRequested, DeepEquals, []restart.RestartType{
		restart.RestartSystem,
		restart.RestartSystem,
	})

	for _, name := range []string{"kernel", "core"} {
		snapID := "kernel-id"
		if name == "core" {
			snapID = "core-snap-id"
		}
		var snapst snapstate.SnapState
		err = snapstate.Get(s.state, name, &snapst)
		c.Assert(err, IsNil)

		c.Assert(snapst.Active, Equals, true)
		c.Assert(snapst.Sequence.Revisions, HasLen, 2)
		c.Assert(snapst.Sequence.Revisions[1], DeepEquals, sequence.NewRevisionSideState(&snap.SideInfo{
			RealName: name,
			Channel:  "latest/stable",
			SnapID:   snapID,
			Revision: snap.R(11),
		}, nil))
	}
}

func (s *snapmgrTestSuite) TestUpdateBaseGadgetKernelSingleReboot(c *C) {
	restore := release.MockOnClassic(false)
	defer restore()
	restore = snapstate.MockRevisionDate(nil)
	defer restore()

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

	var restartRequested []restart.RestartType
	_, err := restart.Manager(s.state, "boot-id-0", snapstatetest.MockRestartHandler(func(t restart.RestartType) {
		restartRequested = append(restartRequested, t)
	}))
	c.Assert(err, IsNil)

	restore = snapstatetest.MockDeviceModel(MakeModel(map[string]any{
		"kernel": "kernel",
		"base":   "core18",
	}))
	defer restore()

	siKernel := snap.SideInfo{
		RealName: "kernel",
		Revision: snap.R(7),
		SnapID:   "kernel-id",
	}
	siBase := snap.SideInfo{
		RealName: "core18",
		Revision: snap.R(7),
		SnapID:   "core18-snap-id",
	}
	siGadget := snap.SideInfo{
		RealName: "gadget",
		Revision: snap.R(7),
		SnapID:   "gadget-core18-id",
	}
	for _, si := range []*snap.SideInfo{&siKernel, &siBase, &siGadget} {
		snaptest.MockSnap(c, fmt.Sprintf(`name: %s`, si.RealName), si)
		typ := "kernel"
		if si.RealName == "core18" {
			typ = "base"
		} else if si.RealName == "gadget" {
			typ = "gadget"
		}
		snapstate.Set(s.state, si.RealName, &snapstate.SnapState{
			Active:          true,
			Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
			Current:         si.Revision,
			TrackingChannel: "latest/stable",
			SnapType:        typ,
		})
	}

	chg := s.state.NewChange("refresh", "refresh kernel, base and gadget")
	affected, tss, err := snapstate.UpdateMany(context.Background(), s.state,
		[]string{"kernel", "core18", "gadget"}, nil, s.user.ID, &snapstate.Flags{})
	c.Assert(err, IsNil)
	c.Assert(affected, DeepEquals, []string{"core18", "gadget", "kernel"})
	var kernelTs, baseTs, gadgetTs *state.TaskSet
	for _, ts := range tss {
		chg.AddAll(ts)
	inner:
		for _, tsk := range ts.Tasks() {
			switch tsk.Kind() {
			// setup-profiles should appear right before link-snap,
			// while set-auto-aliase appears right after
			// auto-connect
			case "link-snap":
				snapsup, err := snapstate.TaskSnapSetup(tsk)
				c.Assert(err, IsNil)
				switch snapsup.InstanceName() {
				case "kernel":
					kernelTs = ts
				case "gadget":
					gadgetTs = ts
				case "core18":
					baseTs = ts
				}
				break inner
			}
		}
	}
	c.Assert(kernelTs, NotNil)
	c.Assert(baseTs, NotNil)
	c.Assert(gadgetTs, NotNil)

	// Core18, gadget and kernel should end up in the same transactional lane
	c.Check(taskSetsShareLane(baseTs, gadgetTs, kernelTs), Equals, true)
	// Grab the tasks we need to check dependencies between
	linkTaskOfBase, err := baseTs.Edge(snapstate.MaybeRebootEdge)
	c.Assert(err, IsNil)
	acTaskOfBase, err := baseTs.Edge(snapstate.MaybeRebootWaitEdge)
	c.Assert(err, IsNil)
	lastTaskOfBase, err := baseTs.Edge(snapstate.EndEdge)
	c.Assert(err, IsNil)
	firstTaskOfGadget := firstTaskAfterLocalModifications(c, gadgetTs)
	mountTaskOfGadget := findKindInTaskSet(gadgetTs, "mount-snap")
	c.Assert(mountTaskOfGadget, NotNil)
	firstPostMountTaskOfGadget := firstTaskAfterMount(c, gadgetTs)
	mountTaskOfBase := findKindInTaskSet(baseTs, "mount-snap")
	c.Assert(mountTaskOfBase, NotNil)
	linkTaskOfGadget, err := gadgetTs.Edge(snapstate.MaybeRebootEdge)
	c.Assert(err, IsNil)
	acTaskOfGadget, err := gadgetTs.Edge(snapstate.MaybeRebootWaitEdge)
	c.Assert(err, IsNil)
	lastTaskOfGadget, err := gadgetTs.Edge(snapstate.EndEdge)
	c.Assert(err, IsNil)
	firstTaskOfKernel := firstTaskAfterLocalModifications(c, kernelTs)
	mountTaskOfKernel := findKindInTaskSet(kernelTs, "mount-snap")
	c.Assert(mountTaskOfKernel, NotNil)
	firstPostMountTaskOfKernel := firstTaskAfterMount(c, kernelTs)
	linkTaskOfKernel, err := kernelTs.Edge(snapstate.MaybeRebootEdge)
	c.Assert(err, IsNil)
	acTaskOfKernel, err := kernelTs.Edge(snapstate.MaybeRebootWaitEdge)
	c.Assert(err, IsNil)

	// Things that must be correct between base and gadget:
	// - first local modification task of gadget must be its prerequisites sync task
	// - the gadget mount task must run after the base mount
	c.Check(firstTaskOfGadget.Kind(), Equals, "prerequisites")
	c.Check(mountTaskOfGadget.WaitTasks(), testutil.Contains, mountTaskOfBase)
	// - the first post-mount task of gadget must depend on the base link
	c.Check(firstPostMountTaskOfGadget.WaitTasks(), testutil.Contains, linkTaskOfBase)
	// - "auto-connect" (MaybeRebootWaitEdge) of base must depend on "link-snap" of kernel (MaybeRebootEdge)
	c.Check(acTaskOfBase.WaitTasks(), testutil.Contains, linkTaskOfKernel)
	// - "auto-connect" (MaybeRebootWaitEdge) of gadget must depend on the last task of base (EndEdge)
	c.Check(acTaskOfGadget.WaitTasks(), testutil.Contains, lastTaskOfBase)

	// Things that must be correct between gadget and kernel:
	// - first local modification task of kernel must be its prerequisites sync task
	// - the kernel mount task must run after the gadget mount
	c.Check(firstTaskOfKernel.Kind(), Equals, "prerequisites")
	c.Check(mountTaskOfKernel.WaitTasks(), testutil.Contains, mountTaskOfGadget)
	// - the first post-mount task of kernel must depend on the gadget link
	c.Check(firstPostMountTaskOfKernel.WaitTasks(), testutil.Contains, linkTaskOfGadget)
	// - "auto-connect" (MaybeRebootWaitEdge) of gadget must depend on last task of base (EndEdge)
	c.Check(acTaskOfGadget.WaitTasks(), testutil.Contains, lastTaskOfBase)
	// - "auto-connect" (MaybeRebootWaitEdge) of kernel must depend on the last task of gadget (EndEdge)
	c.Check(acTaskOfKernel.WaitTasks(), testutil.Contains, lastTaskOfGadget)

	// Manually verify their lanes
	c.Check(linkTaskOfBase.Lanes(), testutil.DeepUnsortedMatches, []int{1, 2, 3})
	c.Check(linkTaskOfGadget.Lanes(), testutil.DeepUnsortedMatches, []int{1, 2, 3})
	c.Check(linkTaskOfKernel.Lanes(), testutil.DeepUnsortedMatches, []int{1, 2, 3})
}

func (s *snapmgrTestSuite) TestUpdateBaseKernelSingleRebootUndone(c *C) {
	restore := release.MockOnClassic(false)
	defer restore()
	restore = snapstate.MockRevisionDate(nil)
	defer restore()

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

	var restartRequested []restart.RestartType
	_, err := restart.Manager(s.state, "boot-id-0", snapstatetest.MockRestartHandler(func(t restart.RestartType) {
		restartRequested = append(restartRequested, t)
	}))
	c.Assert(err, IsNil)

	restore = snapstatetest.MockDeviceModel(ModelWithBase("core18"))
	defer restore()

	// use services-snap here to make sure services would be stopped/started appropriately
	siKernel := snap.SideInfo{
		RealName: "kernel",
		Revision: snap.R(7),
		SnapID:   "kernel-id",
	}
	siBase := snap.SideInfo{
		RealName: "core18",
		Revision: snap.R(7),
		SnapID:   "core18-snap-id",
	}
	for _, si := range []*snap.SideInfo{&siKernel, &siBase} {
		snaptest.MockSnap(c, fmt.Sprintf(`name: %s`, si.RealName), si)
		typ := "kernel"
		if si.RealName == "core18" {
			typ = "base"
		}
		snapstate.Set(s.state, si.RealName, &snapstate.SnapState{
			Active:          true,
			Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
			Current:         si.Revision,
			TrackingChannel: "latest/stable",
			SnapType:        typ,
		})
	}

	chg := s.state.NewChange("refresh", "refresh kernel and base")
	affected, tss, err := snapstate.UpdateMany(context.Background(), s.state,
		[]string{"kernel", "core18"}, nil, s.user.ID, &snapstate.Flags{})
	c.Assert(err, IsNil)
	c.Assert(affected, DeepEquals, []string{"core18", "kernel"})

	// Verify that correct dependencies have been set-up for single-reboot
	// which is a bit more tricky, as task-sets have been split up into pre-boot
	// things, and post-boot things.

	// Grab the correct task-sets
	var baseTs, kernelTs *state.TaskSet
	for _, ts := range tss {
		for _, t := range ts.Tasks() {
			snapsup, err := snapstate.TaskSnapSetup(t)
			if err != nil {
				continue
			}
			if snapsup.Type == snap.TypeKernel {
				kernelTs = ts
				break
			} else if snapsup.Type == snap.TypeBase {
				baseTs = ts
				break
			}
		}
		chg.AddAll(ts)
	}
	c.Assert(baseTs, NotNil)
	c.Assert(kernelTs, NotNil)

	// Grab the tasks we need to check dependencies between
	firstTaskOfKernel := firstTaskAfterLocalModifications(c, kernelTs)
	mountTaskOfKernel := findKindInTaskSet(kernelTs, "mount-snap")
	c.Assert(mountTaskOfKernel, NotNil)
	firstPostMountTaskOfKernel := firstTaskAfterMount(c, kernelTs)
	mountTaskOfBase := findKindInTaskSet(baseTs, "mount-snap")
	c.Assert(mountTaskOfBase, NotNil)
	linkTaskOfKernel, err := kernelTs.Edge(snapstate.MaybeRebootEdge)
	c.Assert(err, IsNil)
	acTaskOfKernel, err := kernelTs.Edge(snapstate.MaybeRebootWaitEdge)
	c.Assert(err, IsNil)
	linkTaskOfBase, err := baseTs.Edge(snapstate.MaybeRebootEdge)
	c.Assert(err, IsNil)
	acTaskOfBase, err := baseTs.Edge(snapstate.MaybeRebootWaitEdge)
	c.Assert(err, IsNil)
	lastTaskOfBase, err := baseTs.Edge(snapstate.EndEdge)
	c.Assert(err, IsNil)

	// Things that must be correct:
	// - first local modification task of kernel must be its prerequisites sync task
	// - the kernel mount task must run after the base mount
	c.Check(firstTaskOfKernel.Kind(), Equals, "prerequisites")
	c.Check(mountTaskOfKernel.WaitTasks(), testutil.Contains, mountTaskOfBase)
	// - the first post-mount task of kernel must depend on the base link
	c.Check(firstPostMountTaskOfKernel.WaitTasks(), testutil.Contains, linkTaskOfBase)
	// - "auto-connect" (MaybeRebootWaitEdge) of base must depend on "link-snap" of kernel (MaybeRebootEdge)
	c.Check(acTaskOfBase.WaitTasks(), testutil.Contains, linkTaskOfKernel)
	// - "auto-connect" (MaybeRebootWaitEdge) of kernel must depend on the last task of base (EndEdge)
	c.Check(acTaskOfKernel.WaitTasks(), testutil.Contains, lastTaskOfBase)

	// have fake backend indicate a need to reboot for both snaps
	s.fakeBackend.linkSnapMaybeReboot = true
	s.fakeBackend.linkSnapRebootFor = map[string]bool{
		"kernel": true,
		"core18": true,
	}
	errInjected := 0
	s.fakeBackend.maybeInjectErr = func(op *fakeOp) error {
		if op.op == "auto-connect:Doing" && op.name == "kernel" {
			errInjected++
			return fmt.Errorf("auto-connect-kernel mock error")
		}
		return nil
	}

	s.settle(c)

	// both snaps have requested a restart at 'auto-connect', handle this here
	s.mockRestartAndSettle(c, chg)

	// both snaps have requested another restart along the undo path at 'unlink-current-snap'
	// because reboots are post-poned until the change have no more tasks to run, and how the
	// change is manipulated in this specific case, we only do one reboot along the undo-path as well now.
	s.mockRestartAndSettle(c, chg)

	c.Check(chg.Status(), Equals, state.ErrorStatus)
	c.Check(chg.Err(), ErrorMatches, `(?s).*\(auto-connect-kernel mock error\)`)
	c.Check(restartRequested, DeepEquals, []restart.RestartType{
		// do path
		restart.RestartSystem,
		// undo
		restart.RestartSystem,
	})
	c.Check(errInjected, Equals, 1)

	// ops come in semi random order, but we know that link and auto-connect
	// operations will be done in a specific order,
	ops := make([]string, 0, 7)
	for _, op := range s.fakeBackend.ops {
		if op.op == "link-snap" {
			split := strings.Split(op.path, "/")
			c.Assert(len(split) > 2, Equals, true)
			ops = append(ops, filepath.Join(split[len(split)-2:]...))
		} else if strings.HasPrefix(op.op, "auto-connect:") {
			ops = append(ops, fmt.Sprintf("%s-%s/%s", op.op, op.name, op.revno))
		}
	}
	c.Assert(ops, HasLen, 7)
	c.Assert(ops[0:5], DeepEquals, []string{
		// link snaps
		"core18/11", "kernel/11",
		"auto-connect:Doing-core18/11",
		"auto-connect:Doing-kernel/11", // fails
		"auto-connect:Undoing-core18/11",
	})
	// those run unordered
	c.Assert(ops[5:], testutil.DeepUnsortedMatches, []string{"core18/7", "kernel/7"})
}

func (s *snapmgrTestSuite) TestUpdateBaseGadgetKernelSingleRebootUndone(c *C) {
	restore := release.MockOnClassic(false)
	defer restore()
	restore = snapstate.MockRevisionDate(nil)
	defer restore()

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

	// make it easier for us to mock the whole gadget update so we don't
	// have to jump through to many hoops.
	s.o.TaskRunner().AddHandler("update-gadget-assets",
		func(task *state.Task, tomb *tomb.Tomb) error {
			task.State().Lock()
			defer task.State().Unlock()
			chg := task.Change()
			chg.Set("gadget-restart-required", true)
			return nil
		},
		func(task *state.Task, tomb *tomb.Tomb) error { return nil })

	s.o.TaskRunner().AddHandler("update-gadget-cmdline",
		func(task *state.Task, tomb *tomb.Tomb) error { return nil },
		func(task *state.Task, tomb *tomb.Tomb) error { return nil })

	var restartRequested []restart.RestartType
	_, err := restart.Manager(s.state, "boot-id-0", snapstatetest.MockRestartHandler(func(t restart.RestartType) {
		restartRequested = append(restartRequested, t)
	}))
	c.Assert(err, IsNil)

	restore = snapstatetest.MockDeviceModel(ModelWithBase("core18"))
	defer restore()

	siKernel := snap.SideInfo{
		RealName: "kernel",
		Revision: snap.R(7),
		SnapID:   "kernel-id",
	}
	siBase := snap.SideInfo{
		RealName: "core18",
		Revision: snap.R(7),
		SnapID:   "core18-snap-id",
	}
	siGadget := snap.SideInfo{
		RealName: "gadget",
		Revision: snap.R(7),
		SnapID:   "gadget-core18-id",
	}
	for _, si := range []*snap.SideInfo{&siKernel, &siBase, &siGadget} {
		snaptest.MockSnap(c, fmt.Sprintf(`name: %s`, si.RealName), si)
		typ := "kernel"
		if si.RealName == "core18" {
			typ = "base"
		} else if si.RealName == "gadget" {
			typ = "gadget"
		}
		snapstate.Set(s.state, si.RealName, &snapstate.SnapState{
			Active:          true,
			Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
			Current:         si.Revision,
			TrackingChannel: "latest/stable",
			SnapType:        typ,
		})
	}

	chg := s.state.NewChange("refresh", "refresh base, gadget and kernel")
	affected, tss, err := snapstate.UpdateMany(context.Background(), s.state,
		[]string{"kernel", "core18", "gadget"}, nil, s.user.ID, &snapstate.Flags{})
	c.Assert(err, IsNil)
	c.Assert(affected, DeepEquals, []string{"core18", "gadget", "kernel"})

	// Grab the correct task-sets
	var baseTs, gadgetTs, kernelTs *state.TaskSet
	for _, ts := range tss {
		for _, t := range ts.Tasks() {
			snapsup, err := snapstate.TaskSnapSetup(t)
			if err != nil {
				continue
			}
			if snapsup.Type == snap.TypeKernel {
				kernelTs = ts
				break
			} else if snapsup.Type == snap.TypeBase {
				baseTs = ts
				break
			} else if snapsup.Type == snap.TypeGadget {
				gadgetTs = ts
				break
			}
		}
		chg.AddAll(ts)
	}
	c.Assert(baseTs, NotNil)
	c.Assert(kernelTs, NotNil)
	c.Assert(gadgetTs, NotNil)

	// have fake backend indicate a need to reboot for both snaps
	s.fakeBackend.linkSnapMaybeReboot = true
	s.fakeBackend.linkSnapRebootFor = map[string]bool{
		"kernel": true,
		"core18": true,
		"gadget": true,
	}
	errInjected := 0
	s.fakeBackend.maybeInjectErr = func(op *fakeOp) error {
		if op.op == "auto-connect:Doing" && op.name == "kernel" {
			errInjected++
			return fmt.Errorf("auto-connect-kernel mock error")
		}
		return nil
	}

	s.settle(c)

	// kernel requests a reboot
	s.mockRestartAndSettle(c, chg)

	// base snap requests another restart along the undo path at 'unlink-current-snap'
	s.mockRestartAndSettle(c, chg)

	checkUndone := func(ts *state.TaskSet, name string) {
		for _, t := range ts.Tasks() {
			switch t.Status() {
			case state.UndoneStatus, state.ErrorStatus, state.HoldStatus:
				continue
			case state.DoneStatus:
				// following tasks don't have undo logic
				switch t.Kind() {
				case "prerequisites", "validate-snap", "run-hook", "cleanup", "update-cert-db":
					break
				default:
					c.Errorf("unexpected done-status for %s task %s", name, t.Kind())
				}
			default:
				c.Errorf("unexpected status for %s task %s: %s", name, t.Kind(), t.Status())
			}
		}
	}

	// Expect all task-sets to have been undone, as essential snaps are considered transactional
	// when updated together. (I.e all are done or all are undone)
	checkUndone(baseTs, "base")
	checkUndone(gadgetTs, "gadget")
	checkUndone(kernelTs, "kernel")

	c.Check(chg.Status(), Equals, state.ErrorStatus)
	c.Check(chg.Err(), ErrorMatches, `(?s).*\(auto-connect-kernel mock error\)`)
	c.Check(restartRequested, DeepEquals, []restart.RestartType{
		// do path
		restart.RestartSystem,
		// undo
		restart.RestartSystem,
	})
	c.Check(errInjected, Equals, 1)
}

func failAfterLinkSnap(ol *overlord.Overlord, chg *state.Change) error {
	err := errors.New("expected")
	ol.TaskRunner().AddHandler("fail", func(*state.Task, *tomb.Tomb) error {
		return err
	}, nil)

	failingTask := ol.State().NewTask("fail", "expected failure")
	chg.AddTask(failingTask)
	linkTask := findLastTask(chg, "link-snap")
	failingTask.WaitFor(linkTask)
	for _, lane := range linkTask.Lanes() {
		failingTask.JoinLane(lane)
	}

	return err
}

func (s *snapmgrTestSuite) testUpdateEssentialSnapsOrder(c *C, order []string) {
	restore := release.MockOnClassic(false)
	defer restore()
	restore = snapstate.MockRevisionDate(nil)
	defer restore()

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

	// make it easier for us to mock the whole gadget update so we don't
	// have to jump through to many hoops. Streamline it a bit as well by
	// making sure that one of the tasks requires a reboot by the link-snap
	s.o.TaskRunner().AddHandler("update-gadget-assets",
		func(task *state.Task, tomb *tomb.Tomb) error {
			task.State().Lock()
			defer task.State().Unlock()
			chg := task.Change()
			chg.Set("gadget-restart-required", true)
			return nil
		},
		func(task *state.Task, tomb *tomb.Tomb) error { return nil })

	s.o.TaskRunner().AddHandler("update-gadget-cmdline",
		func(task *state.Task, tomb *tomb.Tomb) error { return nil },
		func(task *state.Task, tomb *tomb.Tomb) error { return nil })

	var restartRequested []restart.RestartType
	_, err := restart.Manager(s.state, "boot-id-0", snapstatetest.MockRestartHandler(func(t restart.RestartType) {
		restartRequested = append(restartRequested, t)
	}))
	c.Assert(err, IsNil)

	restore = snapstatetest.MockDeviceModel(ModelWithBase("core18"))
	defer restore()

	types := map[string]string{
		"snapd":          "snapd",
		"core18":         "base",
		"gadget":         "gadget",
		"kernel":         "kernel",
		"some-base":      "base",
		"some-base-snap": "app",
	}
	snapIds := map[string]string{
		"snapd":          "snapd-snap-id",
		"core18":         "core18-snap-id",
		"gadget":         "gadget-core18-id",
		"kernel":         "kernel-id",
		"some-base":      "some-base-id",
		"some-base-snap": "some-base-snap-id",
	}

	for _, sn := range order {
		si := &snap.SideInfo{RealName: sn, Revision: snap.R(1), SnapID: snapIds[sn]}
		snapYaml := fmt.Sprintf("name: %s\nversion: 1.2.3\ntype: %s", sn, types[sn])
		if sn == "some-base-snap" {
			// add a base for the app if the base is in the update list
			snapYaml += "\nbase: some-base"
		}

		snaptest.MockSnap(c, snapYaml, si)
		snapstate.Set(s.state, sn, &snapstate.SnapState{
			Active:          true,
			Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
			Current:         si.Revision,
			TrackingChannel: "latest/stable",
			SnapType:        types[sn],
		})
	}

	chg := s.state.NewChange("refresh", fmt.Sprintf("refresh %v", order))
	affected, tss, err := snapstate.UpdateMany(context.Background(), s.state,
		order, nil, s.user.ID, &snapstate.Flags{NoReRefresh: true})
	c.Assert(err, IsNil)
	sort.Strings(affected)
	c.Assert(affected, testutil.DeepUnsortedMatches, order)

	findTs := func(sn string) *state.TaskSet {
		for _, ts := range tss {
			for _, tsk := range ts.Tasks() {
				if tsk.Kind() == "prerequisites" {
					snapsup, err := snapstate.TaskSnapSetup(tsk)
					c.Assert(err, IsNil)
					if snapsup.InstanceName() == sn {
						return ts
					}
					break
				}
			}
		}
		return nil
	}

	for _, ts := range tss {
		chg.AddAll(ts)
	}

	// Map all relevant task-sets.
	tsByName := make(map[string]*state.TaskSet)
	for _, sn := range order {
		ts := findTs(sn)
		c.Assert(ts, NotNil)
		tsByName[sn] = ts
	}

	// Ensure no circular dependency
	c.Check(chg.CheckTaskDependencies(), IsNil)

	var snapdEndTask *state.Task
	if snapdTS := tsByName["snapd"]; snapdTS != nil {
		snapdEndTask, err = snapdTS.Edge(snapstate.EndEdge)
		c.Assert(err, IsNil)
	}

	// Ensure that all reboot participants are correctly linked across both the
	// mount phase and the remaining pre-reboot work.
	var prevRebootTs *state.TaskSet
NextSnap1:
	for _, sn := range order {
	IsRebootSnap:
		switch sn {
		case "core18", "kernel", "gadget":
			break IsRebootSnap
		default:
			continue NextSnap1
		}

		currentTs := tsByName[sn]

		// make sure that downloads happen after snapd is fully done with refresh
		if snapdEndTask != nil {
			beginTaskOfCurrent, err := currentTs.Edge(snapstate.BeginEdge)
			c.Assert(err, IsNil)
			c.Check(beginTaskOfCurrent.WaitTasks(), testutil.Contains, snapdEndTask)
		}

		if prevRebootTs == nil {
			if snapdEndTask != nil {
				mountTaskOfCurrent := findMountSnap(c, currentTs)
				c.Check(waitsOnTransitively(mountTaskOfCurrent, snapdEndTask), Equals, true)
			}
		} else {
			mountTaskOfCurrent := findMountSnap(c, currentTs)
			mountTaskOfPrev := findMountSnap(c, prevRebootTs)
			linkSnapOfPrev, err := prevRebootTs.Edge(snapstate.MaybeRebootEdge)
			c.Assert(err, IsNil)
			c.Check(mountTaskOfCurrent.WaitTasks(), testutil.Contains, mountTaskOfPrev)

			firstPostMountOfCurrent := firstTaskAfterMount(c, currentTs)
			c.Check(firstPostMountOfCurrent.WaitTasks(), testutil.Contains, linkSnapOfPrev)

			beginTaskOfCurrent, err := currentTs.Edge(snapstate.BeginEdge)
			c.Assert(err, IsNil)
			c.Check(beginTaskOfCurrent.WaitTasks(), Not(testutil.Contains), linkSnapOfPrev)
		}
		prevRebootTs = tsByName[sn]
	}

	// Ensure that auto-connect for the reboot-snaps is correctly linked.
	lastRebootTs := prevRebootTs
	var i int
NextSnap2:
	for _, sn := range order {
	IsRebootSnapAC:
		switch sn {
		case "core18", "kernel", "gadget":
			break IsRebootSnapAC
		default:
			continue NextSnap2
		}

		// Skip for last of these, since other task-sets are linked
		// differently.
		currentTs := tsByName[sn]
		if currentTs == lastRebootTs {
			break
		}

		// first "auto-connect" must depend on "link-snap" of previous TS
		if i == 0 {
			acTaskOfCurrent, err := currentTs.Edge(snapstate.MaybeRebootWaitEdge)
			c.Assert(err, IsNil)
			linkSnapOfPrev, err := prevRebootTs.Edge(snapstate.MaybeRebootEdge)
			c.Assert(err, IsNil)
			c.Check(acTaskOfCurrent.WaitTasks(), testutil.Contains, linkSnapOfPrev)
		} else {
			// other "auto-connect" must depend on last task of previous TS
			acTaskOfCurrent, err := currentTs.Edge(snapstate.MaybeRebootWaitEdge)
			c.Assert(err, IsNil)
			lastTaskOfPrev, err := prevRebootTs.Edge(snapstate.EndEdge)
			c.Assert(err, IsNil)
			c.Check(acTaskOfCurrent.WaitTasks(), testutil.Contains, lastTaskOfPrev)
		}
		prevRebootTs = currentTs
		i++
	}

	// Verify by using edges that other non-essential task-sets are correctly
	// connected to the previous task-set.
	var prevTs *state.TaskSet
NextSnap3:
	for _, sn := range order {
		currentTs := tsByName[sn]
	IsEssential:
		switch sn {
		case "snapd", "core18", "kernel", "gadget":
			prevTs = currentTs
			continue NextSnap3
		default:
			break IsEssential
		}

		// Validate the first task-set has no deps
		if prevTs == nil {
			firstTaskOfCurrent, err := currentTs.Edge(snapstate.BeginEdge)
			c.Assert(err, IsNil)
			c.Check(firstTaskOfCurrent.WaitTasks(), HasLen, 0)
			prevTs = currentTs
			continue
		}

		firstTaskOfCurrent, err := currentTs.Edge(snapstate.BeginEdge)
		c.Assert(err, IsNil)
		lastTaskOfPrev, err := prevTs.Edge(snapstate.EndEdge)
		c.Assert(err, IsNil)
		c.Check(firstTaskOfCurrent.WaitTasks(), testutil.Contains, lastTaskOfPrev)
		prevTs = currentTs
	}

	// determine the number of reboots we expect
	s.fakeBackend.linkSnapRebootFor = make(map[string]bool)
	for _, o := range order {
		switch o {
		case "core18", "kernel", "gadget":
			s.fakeBackend.linkSnapRebootFor[o] = true
		}
	}
	s.fakeBackend.linkSnapMaybeReboot = len(s.fakeBackend.linkSnapRebootFor) > 0

	s.settle(c)

	if !s.fakeBackend.linkSnapMaybeReboot {
		// no reboot expected, skip to next checks
	} else {
		c.Check(chg.IsReady(), Equals, false)
		c.Check(chg.Status(), Equals, state.WaitStatus)

		// always one reboot expected
		s.mockRestartAndSettle(c, chg)
	}

	c.Check(chg.IsReady(), Equals, true)
	c.Check(chg.Status(), Equals, state.DoneStatus)
}

func (s *snapmgrTestSuite) TestUpdateEssentialSnapsOrderAll(c *C) {
	s.testUpdateEssentialSnapsOrder(c, []string{"snapd", "core18", "gadget", "kernel"})
}

func (s *snapmgrTestSuite) TestUpdateEssentialSnapsOrderSnapdBase(c *C) {
	s.testUpdateEssentialSnapsOrder(c, []string{"snapd", "core18"})
}

func (s *snapmgrTestSuite) TestUpdateEssentialSnapsOrderBaseGadgetKernel(c *C) {
	s.testUpdateEssentialSnapsOrder(c, []string{"core18", "gadget", "kernel"})
}

func (s *snapmgrTestSuite) TestUpdateEssentialSnapsOrderBaseKernel(c *C) {
	s.testUpdateEssentialSnapsOrder(c, []string{"core18", "kernel"})
}

func (s *snapmgrTestSuite) TestUpdateEssentialSnapsOrderBaseGadget(c *C) {
	s.testUpdateEssentialSnapsOrder(c, []string{"core18", "gadget"})
}

func (s *snapmgrTestSuite) TestUpdateEssentialSnapsOrderBaseGadgetAndSnaps(c *C) {
	s.testUpdateEssentialSnapsOrder(c, []string{"core18", "gadget", "some-base", "some-base-snap"})
}

func (s *snapmgrTestSuite) TestUpdateEssentialSnapsOrderGadgetKernel(c *C) {
	s.testUpdateEssentialSnapsOrder(c, []string{"gadget", "kernel"})
}

func (s *snapmgrTestSuite) TestUpdateEssentialSnapsOrderGadgetKernelAndSnaps(c *C) {
	s.testUpdateEssentialSnapsOrder(c, []string{"gadget", "kernel", "some-base", "some-base-snap"})
}

func (s *snapmgrTestSuite) TestUpdateSnapsOrderSnapdBaseApp(c *C) {
	s.testUpdateEssentialSnapsOrder(c, []string{"snapd", "some-base", "some-base-snap"})
}

func (s *snapmgrTestSuite) TestUpdateSnapsOrderBaseApp(c *C) {
	s.testUpdateEssentialSnapsOrder(c, []string{"some-base", "some-base-snap"})
}

func (s *snapmgrTestSuite) TestUpdateBaseAndSnapdOrder(c *C) {
	// verify that when snapd and a base are updated in one go, the base is
	// set up to have snapd as a prerequisite

	restore := release.MockOnClassic(false)
	defer restore()
	restore = snapstate.MockRevisionDate(nil)
	defer restore()

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

	var restartRequested []restart.RestartType
	_, err := restart.Manager(s.state, "boot-id-0", snapstatetest.MockRestartHandler(func(t restart.RestartType) {
		restartRequested = append(restartRequested, t)
	}))
	c.Assert(err, IsNil)

	restore = snapstatetest.MockDeviceModel(ModelWithBase("core18"))
	defer restore()

	siSnapd := &snap.SideInfo{RealName: "snapd", Revision: snap.R(1), SnapID: "snapd-snap-id"}
	snaptest.MockSnap(c, "name: snapd\nversion: 1.2.3\ntype: snapd", siSnapd)
	snapstate.Set(s.state, "snapd", &snapstate.SnapState{
		Active:          true,
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{siSnapd}),
		Current:         siSnapd.Revision,
		TrackingChannel: "latest/stable",
		SnapType:        "snapd",
	})
	siBase := &snap.SideInfo{RealName: "core18", Revision: snap.R(1), SnapID: "core18-snap-id"}
	snaptest.MockSnap(c, "name: core18\nversion: 1.2.3\ntype: base", siBase)
	snapstate.Set(s.state, "core18", &snapstate.SnapState{
		Active:          true,
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{siBase}),
		Current:         siBase.Revision,
		TrackingChannel: "latest/stable",
		SnapType:        "base",
	})

	chg := s.state.NewChange("refresh", "refresh snapd and base")
	affected, tss, err := snapstate.UpdateMany(context.Background(), s.state,
		[]string{"core18", "snapd"}, nil, s.user.ID, &snapstate.Flags{NoReRefresh: true})
	c.Assert(err, IsNil)
	sort.Strings(affected)
	c.Assert(affected, DeepEquals, []string{"core18", "snapd"})

	// Core18 and snapd are both essential snaps, so we verify that the order between
	// them are correct. Snapd should *always* be updated first, and core18 must have
	// a dependency on snapd

	// grab the task sets of snapd and the base
	var snapdTs, baseTs *state.TaskSet
	for _, ts := range tss {
		chg.AddAll(ts)
		for _, tsk := range ts.Tasks() {
			if tsk.Kind() == "link-snap" {
				snapsup, err := snapstate.TaskSnapSetup(tsk)
				c.Assert(err, IsNil)
				if snapsup.InstanceName() == "snapd" {
					snapdTs = ts
				} else {
					baseTs = ts
				}
			}
		}
	}
	c.Assert(snapdTs, NotNil)
	c.Assert(baseTs, NotNil)

	// Use edges to verify there are correct dependencies
	beginTaskOfBase, err := baseTs.Edge(snapstate.BeginEdge)
	c.Assert(err, IsNil)
	mountTaskOfBase := findMountSnap(c, baseTs)
	lastTaskOfSnapd, err := snapdTs.Edge(snapstate.EndEdge)
	c.Assert(err, IsNil)
	c.Check(beginTaskOfBase.WaitTasks(), testutil.Contains, lastTaskOfSnapd)
	c.Check(waitsOnTransitively(mountTaskOfBase, lastTaskOfSnapd), Equals, true)

	s.fakeBackend.linkSnapMaybeReboot = true
	s.fakeBackend.linkSnapRebootFor = map[string]bool{
		"core18": true,
	}

	s.settle(c)

	// mock restart for the 'link-snap' step and run change to
	// completion.
	s.mockRestartAndSettle(c, chg)

	c.Check(chg.IsReady(), Equals, true)
	c.Check(chg.Status(), Equals, state.DoneStatus)
	c.Check(restartRequested, DeepEquals, []restart.RestartType{
		restart.RestartDaemon,
		restart.RestartSystem,
	})
}

func (s *snapmgrTestSuite) TestUpdateConsidersProvenance(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	snapstate.Set(s.state, "provenance-snap", &snapstate.SnapState{
		Active:          true,
		TrackingChannel: "latest/edge",
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{RealName: "provenance-snap", SnapID: "provenance-snap-id", Revision: snap.R(7)}}),
		Current:         snap.R(7),
		SnapType:        "app",
	})

	ts, err := snapstate.Update(s.state, "provenance-snap", &snapstate.RevisionOptions{Channel: "some-channel"}, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)

	var snapsup snapstate.SnapSetup
	err = ts.Tasks()[0].Get("snap-setup", &snapsup)
	c.Assert(err, IsNil)

	c.Check(snapsup.ExpectedProvenance, Equals, "prov")
}

func (s *snapmgrTestSuite) TestGeneralRefreshSkipsGatedSnaps(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	for _, name := range []string{"some-snap", "some-other-snap"} {
		snapID := fmt.Sprintf("%s-id", name)
		si := &snap.SideInfo{
			RealName: name,
			SnapID:   snapID,
			Revision: snap.R(7),
		}

		snaptest.MockSnap(c, `name: some-snap`, si)
		snapstate.Set(s.state, name, &snapstate.SnapState{
			Active:   true,
			Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
			Current:  si.Revision,
		})
	}

	err := snapstate.HoldRefreshesBySystem(s.state, snapstate.HoldGeneral, "forever", []string{"some-snap"})
	c.Assert(err, IsNil)

	// advance time by a year (the last refreshed is determined by the snap file's
	// timestamp so we can't just mock time.Now() before to pin it)
	plusYearTime := time.Now().Add(365 * 24 * time.Hour)
	restore := snapstate.MockTimeNow(func() time.Time {
		return plusYearTime
	})
	defer restore()

	chg := s.state.NewChange("update", "update all snaps")
	updates, tss, err := snapstate.UpdateMany(context.Background(), s.state, nil, nil, s.user.ID, nil)
	c.Check(err, IsNil)
	c.Check(updates, DeepEquals, []string{"some-other-snap"})

	for _, ts := range tss {
		chg.AddAll(ts)
	}

	s.settle(c)

	c.Check(chg.Err(), IsNil)
	c.Check(chg.IsReady(), Equals, true)
	c.Check(chg.Status(), Equals, state.DoneStatus)
}

func (s *snapmgrTestSuite) TestSpecificRefreshRefreshesHeldSnaps(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	for _, name := range []string{"some-snap", "some-other-snap"} {
		snapID := fmt.Sprintf("%s-id", name)
		si := &snap.SideInfo{
			RealName: name,
			SnapID:   snapID,
			Revision: snap.R(7),
		}

		snaptest.MockSnap(c, `name: some-snap`, si)
		snapstate.Set(s.state, name, &snapstate.SnapState{
			Active:   true,
			Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
			Current:  si.Revision,
		})
	}

	err := snapstate.HoldRefreshesBySystem(s.state, snapstate.HoldGeneral, "forever", []string{"some-snap"})
	c.Assert(err, IsNil)

	chg := s.state.NewChange("update", "update all snaps")
	updates, tss, err := snapstate.UpdateMany(context.Background(), s.state, []string{"some-other-snap", "some-snap"}, nil, s.user.ID, nil)
	c.Check(err, IsNil)
	c.Check(updates, DeepEquals, []string{"some-other-snap", "some-snap"})

	for _, ts := range tss {
		chg.AddAll(ts)
	}

	s.settle(c)

	c.Check(chg.Err(), IsNil)
	c.Check(chg.IsReady(), Equals, true)
	c.Check(chg.Status(), Equals, state.DoneStatus)
}

func (s *snapmgrTestSuite) TestUpdateManyTransactionalWithLane(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	for _, name := range []string{"some-snap", "some-other-snap"} {
		snapID := fmt.Sprintf("%s-id", name)
		si := &snap.SideInfo{
			RealName: name,
			SnapID:   snapID,
			Revision: snap.R(7),
		}

		snaptest.MockSnap(c, `name: some-snap`, si)
		snapstate.Set(s.state, name, &snapstate.SnapState{
			Active:   true,
			Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
			Current:  si.Revision,
		})
	}

	lane := s.state.NewLane()
	flags := &snapstate.Flags{
		Transaction: client.TransactionAllSnaps,
		Lane:        lane,
		// the check rerefresh taskset doesn't run in the same lane
		NoReRefresh: true,
	}
	affected, tss, err := snapstate.UpdateMany(context.Background(), s.state, []string{"some-snap", "some-other-snap"}, nil, s.user.ID, flags)
	c.Assert(err, IsNil)
	c.Check(affected, testutil.DeepUnsortedMatches, []string{"some-snap", "some-other-snap"})
	c.Check(tss, HasLen, 3)

	for _, ts := range tss {
		for _, t := range ts.Tasks() {
			c.Assert(t.Lanes(), DeepEquals, []int{lane})
		}
	}

	verifyDelayedEffectsTasks(c, tss[2], []int{lane}, lane)
}

func (s *snapmgrTestSuite) TestUpdateManyNoDelayedEffects(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	for _, name := range []string{"some-snap", "some-other-snap"} {
		snapID := fmt.Sprintf("%s-id", name)
		si := &snap.SideInfo{
			RealName: name,
			SnapID:   snapID,
			Revision: snap.R(7),
		}

		snaptest.MockSnap(c, `name: some-snap`, si)
		snapstate.Set(s.state, name, &snapstate.SnapState{
			Active:   true,
			Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
			Current:  si.Revision,
		})
	}

	flags := &snapstate.Flags{
		NoDelayedSideEffects: true,
	}
	affected, tss, err := snapstate.UpdateMany(context.Background(), s.state, []string{"some-snap", "some-other-snap"}, nil, s.user.ID, flags)
	c.Assert(err, IsNil)
	c.Check(affected, testutil.DeepUnsortedMatches, []string{"some-snap", "some-other-snap"})
	c.Check(tss, HasLen, 3)
	verifyReRefreshTasks(c, tss[2])
}

func (s *snapmgrTestSuite) TestUpdateManyLaneErrorsWithLaneButNoTransaction(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	for _, name := range []string{"some-snap", "some-other-snap"} {
		snapID := fmt.Sprintf("%s-id", name)
		si := &snap.SideInfo{
			RealName: name,
			SnapID:   snapID,
			Revision: snap.R(7),
		}

		snaptest.MockSnap(c, `name: some-snap`, si)
		snapstate.Set(s.state, name, &snapstate.SnapState{
			Active:   true,
			Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
			Current:  si.Revision,
		})
	}

	lane := s.state.NewLane()
	flags := &snapstate.Flags{
		Lane: lane,
	}

	affected, tss, err := snapstate.UpdateMany(context.Background(), s.state, []string{"some-snap", "some-other-snap"}, nil, s.user.ID, flags)
	c.Assert(err, ErrorMatches, "cannot specify a lane without setting transaction to \"all-snaps\"")
	c.Check(tss, IsNil)
	c.Check(affected, IsNil)

	flags.Transaction = client.TransactionPerSnap

	affected, tss, err = snapstate.UpdateMany(context.Background(), s.state, []string{"some-snap", "some-other-snap"}, nil, s.user.ID, flags)
	c.Assert(err, ErrorMatches, "cannot specify a lane without setting transaction to \"all-snaps\"")
	c.Check(tss, IsNil)
	c.Check(affected, IsNil)
}

func (s *snapmgrTestSuite) TestUpdateTransactionalWithLane(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	si := &snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Revision: snap.R(7),
	}

	snaptest.MockSnap(c, `name: some-snap`, si)
	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
		Current:  si.Revision,
	})

	lane := s.state.NewLane()
	flags := &snapstate.Flags{
		Transaction: client.TransactionAllSnaps,
		Lane:        lane,
		// the check rerefresh taskset doesn't run in the same lane
		NoReRefresh: true,
	}

	ts, err := snapstate.Update(s.state, "some-snap", nil, s.user.ID, *flags)
	c.Assert(err, IsNil)
	c.Assert(ts, Not(IsNil))
	for _, t := range ts.Tasks() {
		c.Assert(t.Lanes(), DeepEquals, []int{lane})
	}

	verifyUpdateTasks(c, snap.TypeApp, mockDelayedEffects, 0, ts)
}

func (s *snapmgrTestSuite) TestUpdateLaneErrorsWithLaneButNoTransaction(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	lane := s.state.NewLane()
	flags := &snapstate.Flags{
		Lane: lane,
	}

	si := &snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Revision: snap.R(7),
	}

	snaptest.MockSnap(c, `name: some-snap`, si)
	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
		Current:  si.Revision,
	})

	ts, err := snapstate.Update(s.state, "some-snap", nil, s.user.ID, *flags)
	c.Assert(err, ErrorMatches, "cannot specify a lane without setting transaction to \"all-snaps\"")
	c.Check(ts, IsNil)

	flags.Transaction = client.TransactionPerSnap
	ts, err = snapstate.Update(s.state, "some-snap", nil, s.user.ID, *flags)
	c.Assert(err, ErrorMatches, "cannot specify a lane without setting transaction to \"all-snaps\"")
	c.Check(ts, IsNil)
}

func (s *snapmgrTestSuite) TestConditionalAutoRefreshCreatesPreDownloadChange(c *C) {
	restore := snapstate.MockRefreshAppsCheck(func(si *snap.Info) error {
		return snapstate.NewBusySnapError(si, []int{123}, nil, nil)
	})
	defer restore()

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

	si := &snap.SideInfo{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)}
	snapst := &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
		Current:  si.Revision,
		SnapType: string(snap.TypeApp),
	}
	snapstate.Set(s.state, "some-snap", snapst)

	chg := s.state.NewChange("auto-refresh", "test change")
	task := s.state.NewTask("conditional-auto-refresh", "test task")
	chg.AddTask(task)

	snapsup := &snapstate.SnapSetup{
		SideInfo: &snap.SideInfo{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(2)},
		Flags:    snapstate.Flags{IsAutoRefresh: true},
	}
	task.Set("snap-setup", snapsup)
	task.Set("snaps", map[string]*snapstate.RefreshCandidate{
		"some-snap": {
			SnapSetup: *snapsup,
		}})

	s.settle(c)

	chgs := s.state.Changes()
	// sort "auto-refresh" into first and "pre-download" into second
	sort.Slice(chgs, func(i, j int) bool {
		return chgs[i].Kind() < chgs[j].Kind()
	})

	c.Assert(chgs, HasLen, 2)
	c.Assert(chgs[0].Err(), IsNil)
	c.Assert(chgs[0].Status(), Equals, state.DoneStatus)

	checkPreDownloadChange(c, chgs[1], "some-snap", snap.R(2))
}

func (s *snapmgrTestSuite) TestAutoRefreshCreatePreDownload(c *C) {
	restore := snapstate.MockRefreshAppsCheck(func(si *snap.Info) error {
		return snapstate.NewBusySnapError(si, []int{123}, nil, nil)
	})
	defer restore()

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

	si := &snap.SideInfo{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)}
	snapst := &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
		Current:  si.Revision,
		SnapType: string(snap.TypeApp),
	}
	snapstate.Set(s.state, "some-snap", snapst)
	snapsup := &snapstate.SnapSetup{
		SideInfo: &snap.SideInfo{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(2)},
		Flags:    snapstate.Flags{IsAutoRefresh: true},
	}

	ts, err := snapstate.DoInstallOrPreDownload(s.state, snapst, snapsup, nil, snapstate.InstallContext{})

	var busyErr *snapstate.TimedBusySnapError
	c.Assert(errors.As(err, &busyErr), Equals, true)
	refreshInfo := busyErr.PendingSnapRefreshInfo()
	c.Check(refreshInfo, DeepEquals, &userclient.PendingSnapRefreshInfo{
		InstanceName:  "some-snap",
		TimeRemaining: snapstate.MaxInhibitionDuration(s.state),
	})

	tasks := ts.TaskSet().Tasks()
	c.Assert(tasks, HasLen, 1)
	c.Assert(tasks[0].Kind(), Equals, "pre-download-snap")
	c.Assert(tasks[0].Summary(), testutil.Contains, "Pre-download snap \"some-snap\" (2) from channel")
}

func (s *snapmgrTestSuite) testAutoRefreshRecordsFailures(c *C, afterReboot bool) {
	badSnap := "some-snap"
	badSnapID := "some-snap-id"
	badSnapType := snap.TypeApp
	expectedMinDelay := 8
	expectedMaxDelay := 336
	expectedFailureSeverity := snap.RefreshFailureSeverityNone
	if afterReboot {
		badSnap = "kernel"
		badSnapID = "kernel-id"
		badSnapType = snap.TypeKernel
		expectedFailureSeverity = snap.RefreshFailureSeverityAfterReboot
		// backoff delay is more aggressive for snaps failing after reboot
		expectedMinDelay = 16
		expectedMaxDelay = 672
	}

	// Setup test snaps
	s.state.Lock()
	defer s.state.Unlock()
	snapstate.Set(s.state, badSnap, &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: badSnap, SnapID: badSnapID, Revision: snap.R(1)},
		}),
		Current:  snap.R(1),
		SnapType: string(badSnapType),
	})
	snapstate.Set(s.state, "some-other-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-other-snap", SnapID: "some-other-snap-id", Revision: snap.R(1)},
		}),
		Current:  snap.R(1),
		SnapType: string(snap.TypeApp),
	})

	snapstate.CanAutoRefresh = func(*state.State) (bool, error) { return true, nil }

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

	badSnapRevision := snap.R(12)
	goodSnapRevision := snap.R(12)
	// Specify retrieved revisions for snaps from fakestore
	s.fakeStore.refreshRevnos[badSnapID] = badSnapRevision
	s.fakeStore.refreshRevnos["some-other-snap-id"] = goodSnapRevision

	getRefreshFailures := func(name string) *snap.RefreshFailuresInfo {
		var snapst snapstate.SnapState
		c.Assert(snapstate.Get(s.state, name, &snapst), IsNil)
		return snapst.RefreshFailures
	}
	getRefreshFailed := func(chg *state.Change) []any {
		var apiData map[string]any
		c.Assert(chg.Get("api-data", &apiData), IsNil)
		if apiData["refresh-failed"] == nil {
			return nil
		}
		refreshFailed := apiData["refresh-failed"].([]any)
		return refreshFailed
	}
	getRevision := func(name string) snap.Revision {
		var snapst snapstate.SnapState
		c.Assert(snapstate.Get(s.state, name, &snapst), IsNil)
		return snapst.Current
	}

	forceAutoRefresh := func() {
		s.state.Unlock()
		// Fake nextRefresh to now.
		s.snapmgr.MockNextRefresh(time.Now())
		// Fake that the retryRefreshDelay is over
		restore := snapstate.MockRefreshRetryDelay(1 * time.Millisecond)
		defer restore()
		time.Sleep(10 * time.Millisecond)
		// Trigger autorefresh.Ensure().
		err := s.snapmgr.Ensure()
		if errors.Is(err, advisor.ErrNotSupported) {
			c.Skip("bolt is not supported")
		}
		c.Assert(err, IsNil)
		s.state.Lock()
		s.settle(c)
	}

	// Auto-refresh errors are not recorded until after unlink-current-snap as
	// this indicates (with high probability) a real issue with the snap revision.
	s.fakeBackend.maybeInjectErr = func(fo *fakeOp) error {
		if fo.op == "unlink-snap" && strings.Contains(fo.path, badSnap) {
			return errors.New("error exactly at unlink-current-snap")
		}
		return nil
	}
	buf.Reset()
	forceAutoRefresh()
	chgs := s.state.Changes()
	c.Assert(chgs, HasLen, 1)
	c.Check(chgs[0].Kind(), Equals, "auto-refresh")
	expectedUnlinkErrPattern := fmt.Sprintf("cannot perform the following tasks:\n.*- Make current revision for snap %q unavailable \\(error exactly at unlink-current-snap\\)", badSnap)
	c.Check(chgs[0].Err(), ErrorMatches, expectedUnlinkErrPattern)
	c.Check(chgs[0].Status(), Equals, state.ErrorStatus)
	c.Check(buf.String(), Not(testutil.Contains), fmt.Sprintf("snap %q auto-refresh to revision 12 has failed, next auto-refresh attempt will be in %d hours", badSnap, expectedMinDelay))
	c.Check(getRefreshFailed(chgs[0]), IsNil)
	c.Check(getRefreshFailures(badSnap), IsNil) // Refresh failures only count after unlink-current-snap
	c.Check(getRevision(badSnap), Equals, snap.R(1))
	c.Check(getRefreshFailures("some-other-snap"), IsNil)
	c.Check(getRevision("some-other-snap"), Equals, goodSnapRevision)

	// Auto-refresh errors are recorded after unlink-current-snap
	goodSnapRevision.N++
	s.fakeStore.refreshRevnos["some-other-snap-id"] = goodSnapRevision
	// Inject error in auto-connect (i.e after link-snap) so we should have
	// rebooted by now for essential snaps (e.g. kernel snap).
	s.fakeBackend.maybeInjectErr = func(op *fakeOp) error {
		if op.op == "auto-connect:Doing" && op.name == badSnap {
			return fmt.Errorf("auto-connect mock error")
		}
		return nil
	}
	buf.Reset()
	forceAutoRefresh()
	chgs = s.state.Changes()
	sort.Slice(chgs, func(i, j int) bool {
		return chgs[i].SpawnTime().Before(chgs[j].SpawnTime())
	})
	c.Assert(chgs, HasLen, 2)
	c.Check(chgs[1].Kind(), Equals, "auto-refresh")
	expectedAutoConnectErrPattern := fmt.Sprintf("cannot perform the following tasks:\n.*- Automatically connect eligible plugs and slots of snap %q \\(auto-connect mock error\\)", badSnap)
	c.Check(chgs[1].Err(), ErrorMatches, expectedAutoConnectErrPattern)
	c.Check(chgs[1].Status(), Equals, state.ErrorStatus)
	c.Check(buf.String(), testutil.Contains, fmt.Sprintf("snap %q auto-refresh to revision 12 has failed, next auto-refresh attempt will be delayed by %d hours", badSnap, expectedMinDelay))
	c.Check(getRefreshFailed(chgs[1]), DeepEquals, []any{badSnap})
	c.Check(getRefreshFailures(badSnap).Revision, Equals, badSnapRevision)
	c.Check(getRefreshFailures(badSnap).FailureCount, Equals, 1)
	c.Check(getRefreshFailures(badSnap).LastFailureSeverity, Equals, expectedFailureSeverity)
	c.Check(getRevision(badSnap), Equals, snap.R(1))
	c.Check(getRefreshFailures("some-other-snap"), IsNil)
	c.Check(getRevision("some-other-snap"), Equals, goodSnapRevision)

	// Bad snap refresh is skipped due to backoff delay, but other refresh continue normally
	goodSnapRevision.N++
	s.fakeStore.refreshRevnos["some-other-snap-id"] = goodSnapRevision
	buf.Reset()
	forceAutoRefresh()
	chgs = s.state.Changes()
	sort.Slice(chgs, func(i, j int) bool {
		return chgs[i].SpawnTime().Before(chgs[j].SpawnTime())
	})
	c.Assert(chgs, HasLen, 3)
	c.Check(chgs[2].Kind(), Equals, "auto-refresh")
	c.Check(chgs[2].Err(), IsNil)
	c.Check(chgs[2].Status(), Equals, state.DoneStatus)
	c.Check(buf.String(), Not(testutil.Contains), "auto-refresh to revision 12 has failed")
	c.Check(getRefreshFailed(chgs[2]), IsNil)
	c.Check(getRefreshFailures(badSnap).Revision, Equals, badSnapRevision)
	c.Check(getRefreshFailures(badSnap).FailureCount, Equals, 1) // Stays at 1
	c.Check(getRefreshFailures(badSnap).LastFailureSeverity, Equals, expectedFailureSeverity)
	c.Check(getRevision(badSnap), Equals, snap.R(1))
	c.Check(getRefreshFailures("some-other-snap"), IsNil)
	c.Check(getRevision("some-other-snap"), Equals, goodSnapRevision)

	// Make sure backoff delay is capped to a reasonable value (~2 weeks)
	var snapst snapstate.SnapState
	c.Assert(snapstate.Get(s.state, badSnap, &snapst), IsNil)
	snapst.RefreshFailures = &snap.RefreshFailuresInfo{
		Revision:        badSnapRevision,
		FailureCount:    100,
		LastFailureTime: time.Now().Add(-time.Duration(expectedMaxDelay) * time.Hour),
	}
	snapstate.Set(s.state, badSnap, &snapst)
	buf.Reset()
	forceAutoRefresh()
	chgs = s.state.Changes()
	sort.Slice(chgs, func(i, j int) bool {
		return chgs[i].SpawnTime().Before(chgs[j].SpawnTime())
	})
	c.Assert(chgs, HasLen, 4)
	c.Check(chgs[3].Kind(), Equals, "auto-refresh")
	c.Check(chgs[3].Err(), ErrorMatches, expectedAutoConnectErrPattern)
	c.Check(chgs[3].Status(), Equals, state.ErrorStatus)
	c.Check(buf.String(), testutil.Contains, fmt.Sprintf("snap %q auto-refresh to revision 12 has failed, next auto-refresh attempt will be delayed by %d hours", badSnap, expectedMaxDelay))
	c.Check(getRefreshFailed(chgs[3]), DeepEquals, []any{badSnap})
	c.Check(getRefreshFailures(badSnap).Revision, Equals, badSnapRevision)
	c.Check(getRefreshFailures(badSnap).FailureCount, Equals, 101) // Backoff delay passed, increment for new failure
	c.Check(getRefreshFailures(badSnap).LastFailureSeverity, Equals, expectedFailureSeverity)
	c.Check(getRevision(badSnap), Equals, snap.R(1))

	// New revision resets RefreshFailures
	s.fakeStore.refreshRevnos[badSnapID] = snap.R(13)
	s.fakeBackend.maybeInjectErr = nil
	buf.Reset()
	forceAutoRefresh()
	chgs = s.state.Changes()
	sort.Slice(chgs, func(i, j int) bool {
		return chgs[i].SpawnTime().Before(chgs[j].SpawnTime())
	})
	c.Assert(chgs, HasLen, 5)
	c.Check(chgs[4].Kind(), Equals, "auto-refresh")
	c.Check(chgs[4].Err(), IsNil)
	c.Check(chgs[4].Status(), Equals, state.DoneStatus)
	c.Check(getRefreshFailed(chgs[4]), IsNil)
	c.Check(getRefreshFailures(badSnap), IsNil)
	c.Check(getRevision(badSnap), Equals, snap.R(13))
}

func (s *snapmgrTestSuite) TestAutoRefreshRecordsFailures(c *C) {
	const afterReboot = false
	s.testAutoRefreshRecordsFailures(c, afterReboot)
}

func (s *snapmgrTestSuite) TestAutoRefreshRecordsFailuresAfterReboot(c *C) {
	const afterReboot = true
	s.testAutoRefreshRecordsFailures(c, afterReboot)
}

func (s *snapmgrTestSuite) testAutoRefreshRefreshInhibitNoticeRecorded(c *C, markerInterfaceConnected bool, warningFallback bool) {
	refreshAppsCheckCalled := 0
	restore := snapstate.MockRefreshAppsCheck(func(si *snap.Info) error {
		refreshAppsCheckCalled++
		if si.SnapID == "some-snap-id" {
			return snapstate.NewBusySnapError(si, []int{123}, nil, nil)
		}
		return nil
	})
	defer restore()

	var monitored int
	restore = snapstate.MockCgroupMonitorSnapEnded(func(string, chan<- string) error {
		monitored++
		return nil
	})
	defer restore()

	var connCheckCalled int
	restore = snapstate.MockHasActiveConnection(func(st *state.State, iface string) (bool, error) {
		connCheckCalled++
		c.Check(iface, Equals, "snap-refresh-observe")
		return markerInterfaceConnected, nil
	})
	defer restore()

	// let's add some random warnings
	s.state.Lock()
	s.state.Warnf("this is a random warning 1")
	s.state.Warnf("this is a random warning 2")
	s.state.Unlock()

	s.state.Lock()
	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)},
		}),
		Current:  snap.R(1),
		SnapType: string(snap.TypeApp),
	})
	snapstate.Set(s.state, "some-other-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-other-snap", SnapID: "some-other-snap-id", Revision: snap.R(1)},
		}),
		Current:  snap.R(1),
		SnapType: string(snap.TypeApp),
	})
	s.state.Unlock()

	snapstate.CanAutoRefresh = func(*state.State) (bool, error) { return true, nil }
	// Trigger autorefresh.Ensure().
	err := s.snapmgr.Ensure()
	if errors.Is(err, advisor.ErrNotSupported) {
		c.Skip("bolt is not supported")
	}
	c.Assert(err, IsNil)

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

	chgs := s.state.Changes()
	sort.Slice(chgs, func(i, j int) bool {
		return chgs[i].Kind() < chgs[j].Kind()
	})
	c.Assert(chgs, HasLen, 2)
	c.Check(chgs[1].Kind(), Equals, "pre-download")
	c.Check(chgs[1].Status(), Equals, state.DoStatus)
	c.Check(chgs[0].Kind(), Equals, "auto-refresh")
	c.Check(chgs[0].Status(), Equals, state.DoStatus)
	// No notices or warnings are recorded until auto-refresh change is marked as ready.
	checkRefreshInhibitNotice(c, s.state, 0)
	// no "refresh inhibition" warnings recorded
	checkNoRefreshInhibitWarning(c, s.state)

	s.settle(c)

	// - Twice in soft-check of original auto-refresh (once per snap).
	// - Once in pre-download (for some-snap).
	// - Once in hard-check of original auto-refresh (for some-other-snap).
	c.Check(refreshAppsCheckCalled, Equals, 4)
	// Pre-downloaded snap is monitored.
	c.Check(monitored, Equals, 1)

	chgs = s.state.Changes()
	sort.Slice(chgs, func(i, j int) bool {
		return chgs[i].Kind() < chgs[j].Kind()
	})
	c.Assert(chgs, HasLen, 2)
	c.Check(chgs[1].Kind(), Equals, "pre-download")
	c.Check(chgs[1].Status(), Equals, state.DoneStatus)
	// A continued auto-refresh shouldn't be created because snap is still monitored.
	c.Check(chgs[0].Kind(), Equals, "auto-refresh")
	c.Check(chgs[0].Status(), Equals, state.DoneStatus)

	// Aggregate notice and warning is recorded when auto-refresh change is marked as ready.
	checkRefreshInhibitNotice(c, s.state, 1)
	if warningFallback {
		checkRefreshInhibitWarning(c, s.state, []string{"some-snap"}, time.Time{})
	} else {
		checkNoRefreshInhibitWarning(c, s.state)
	}
}

func (s *snapmgrTestSuite) TestAutoRefreshRefreshInhibitNoticeRecorded(c *C) {
	s.enableRefreshAppAwarenessUX()
	const markerInterfaceConnected = true
	const warningFallback = false
	s.testAutoRefreshRefreshInhibitNoticeRecorded(c, markerInterfaceConnected, warningFallback)
}

func (s *snapmgrTestSuite) TestAutoRefreshRefreshInhibitNoticeRecordedWarningFallback(c *C) {
	s.enableRefreshAppAwarenessUX()
	const markerInterfaceConnected = false
	const warningFallback = true
	s.testAutoRefreshRefreshInhibitNoticeRecorded(c, markerInterfaceConnected, warningFallback)
}

func (s *snapmgrTestSuite) TestAutoRefreshRefreshInhibitNoticeRecordedWarningFallbackNoRAAUX(c *C) {
	const markerInterfaceConnected = false
	const warningFallback = false
	s.testAutoRefreshRefreshInhibitNoticeRecorded(c, markerInterfaceConnected, warningFallback)
}

func (s *snapmgrTestSuite) TestAutoRefreshRefreshInhibitNoticeRecordedOnPreDownloadOnly(c *C) {
	refreshAppsCheckCalled := 0
	restore := snapstate.MockRefreshAppsCheck(func(si *snap.Info) error {
		refreshAppsCheckCalled++
		if refreshAppsCheckCalled == 1 {
			return snapstate.NewBusySnapError(si, []int{123}, nil, nil)
		}
		return nil
	})
	defer restore()

	seq := snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
		{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)},
	})
	seq.AddComponentForRevision(snap.R(1), &sequence.ComponentState{
		SideInfo: &snap.ComponentSideInfo{
			Component: naming.NewComponentRef("some-snap", "standard-component"),
			Revision:  snap.R(1),
		},
		CompType: snap.StandardComponent,
	})

	s.state.Lock()
	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:          true,
		Sequence:        seq,
		Current:         snap.R(1),
		TrackingChannel: "channel-for-components",
		SnapType:        string(snap.TypeApp),
	})
	s.state.Unlock()

	s.fakeStore.snapResourcesFn = func(info *snap.Info) []store.SnapResourceResult {
		c.Assert(info.InstanceName(), Equals, "some-snap")
		return []store.SnapResourceResult{
			{
				Name:      "standard-component",
				Type:      "component/standard",
				Version:   "1.0",
				CreatedAt: "2024-01-01T00:00:00Z",
				Revision:  5,
			},
		}
	}

	s.AddCleanup(snapstate.MockReadComponentInfo(func(
		compMntDir string, info *snap.Info, csi *snap.ComponentSideInfo,
	) (*snap.ComponentInfo, error) {
		return &snap.ComponentInfo{
			Component:         csi.Component,
			Type:              "standard",
			CompVersion:       "1.0",
			ComponentSideInfo: *csi,
		}, nil
	}))

	snapstate.CanAutoRefresh = func(*state.State) (bool, error) { return true, nil }
	// Trigger autorefresh.Ensure().
	err := s.snapmgr.Ensure()
	if errors.Is(err, advisor.ErrNotSupported) {
		c.Skip("bolt is not supported")
	}
	c.Assert(err, IsNil)

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

	chgs := s.state.Changes()
	c.Assert(chgs, HasLen, 1)
	c.Check(chgs[0].Kind(), Equals, "pre-download")
	c.Check(chgs[0].Status(), Equals, state.DoStatus)
	// If all refresh candidates are blocked from auto-refresh and only a
	// pre-download change is created for those snaps we should still record
	// a notice.
	checkRefreshInhibitNotice(c, s.state, 1)

	s.settle(c)

	// - Once in soft-check of original auto-refresh.
	// - Once in pre-download.
	// - Once in soft-check of continued auto-refresh.
	// - Once in hard-check of continued auto-refresh.
	c.Check(refreshAppsCheckCalled, Equals, 4)

	chgs = s.state.Changes()
	sort.Slice(chgs, func(i, j int) bool {
		return chgs[i].Kind() < chgs[j].Kind()
	})

	c.Assert(chgs, HasLen, 2)
	c.Check(chgs[1].Kind(), Equals, "pre-download")
	c.Check(chgs[1].Status(), Equals, state.DoneStatus)
	// Continued auto-refresh.
	c.Check(chgs[0].Kind(), Equals, "auto-refresh")
	c.Check(chgs[0].Status(), Equals, state.DoneStatus)

	// No more snaps are marked as inhibited after the continued auto-refresh
	// The set of inhibtied snaps changed to an empty set, so another notice is recorded.
	checkRefreshInhibitNotice(c, s.state, 2)

	var snapst snapstate.SnapState
	err = snapstate.Get(s.state, "some-snap", &snapst)
	c.Assert(err, IsNil)
	c.Check(snapst.Current, Equals, snap.R(11))

	csi := snapst.CurrentComponentSideInfo(naming.NewComponentRef("some-snap", "standard-component"))
	c.Check(csi, NotNil)
	c.Check(csi.Revision, Equals, snap.R(5))
}

func (s *snapmgrTestSuite) TestAutoRefreshRefreshInhibitNoticeNotRecorded(c *C) {
	s.state.Lock()
	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)},
		}),
		Current:  snap.R(1),
		SnapType: string(snap.TypeApp),
	})
	snapstate.Set(s.state, "some-other-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-other-snap", SnapID: "some-other-snap-id", Revision: snap.R(1)},
		}),
		Current:  snap.R(1),
		SnapType: string(snap.TypeApp),
	})
	s.state.Unlock()

	snapstate.CanAutoRefresh = func(*state.State) (bool, error) { return true, nil }
	// Trigger autorefresh.Ensure().
	err := s.snapmgr.Ensure()
	if errors.Is(err, advisor.ErrNotSupported) {
		c.Skip("bolt is not supported")
	}
	c.Assert(err, IsNil)

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

	chgs := s.state.Changes()
	c.Assert(chgs, HasLen, 1)
	c.Check(chgs[0].Kind(), Equals, "auto-refresh")
	c.Check(chgs[0].Status(), Equals, state.DoStatus)
	// No notices are recorded.
	checkRefreshInhibitNotice(c, s.state, 0)

	s.settle(c)

	chgs = s.state.Changes()
	c.Assert(chgs, HasLen, 1)
	c.Check(chgs[0].Kind(), Equals, "auto-refresh")
	c.Check(chgs[0].Status(), Equals, state.DoneStatus)
	// No notices are recorded because no snaps are blocked.
	checkRefreshInhibitNotice(c, s.state, 0)
}

func (s *snapmgrTestSuite) TestAutoRefreshRefreshInhibitNoticeRecordedOnce(c *C) {
	refreshAppsCheckCalled := 0
	restore := snapstate.MockRefreshAppsCheck(func(si *snap.Info) error {
		refreshAppsCheckCalled++
		return snapstate.NewBusySnapError(si, []int{123}, nil, nil)
	})
	defer restore()

	// Never close monitoring channel to avoid triggering a continued auto-refresh
	var monitored int
	restore = snapstate.MockCgroupMonitorSnapEnded(func(string, chan<- string) error {
		monitored++
		return nil
	})
	defer restore()

	// Setup test snaps
	s.state.Lock()
	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)},
		}),
		Current:  snap.R(1),
		SnapType: string(snap.TypeApp),
	})
	// Verify list is empty
	checkLastRecordedInhibitedSnaps(c, s.state, nil)
	s.state.Unlock()

	snapstate.CanAutoRefresh = func(*state.State) (bool, error) { return true, nil }
	// Trigger autorefresh.Ensure().
	err := s.snapmgr.Ensure()
	if errors.Is(err, advisor.ErrNotSupported) {
		c.Skip("bolt is not supported")
	}
	c.Assert(err, IsNil)

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

	// First auto-refresh attempt
	s.settle(c)
	chgs := s.state.Changes()
	c.Assert(chgs, HasLen, 1)
	c.Check(chgs[0].Kind(), Equals, "pre-download")
	c.Check(chgs[0].Status(), Equals, state.DoneStatus)
	// If all refresh candidates are blocked from auto-refresh and only a
	// pre-download change is created for those snaps we should still record
	// a notice.
	checkRefreshInhibitNotice(c, s.state, 1)
	checkLastRecordedInhibitedSnaps(c, s.state, []string{"some-snap"})

	forceAutoRefresh := func() {
		s.state.Unlock()
		// Fake nextRefresh to now.
		s.snapmgr.MockNextRefresh(time.Now())
		// Fake that the retryRefreshDelay is over
		restore = snapstate.MockRefreshRetryDelay(1 * time.Millisecond)
		defer restore()
		time.Sleep(10 * time.Millisecond)
		// Trigger autorefresh.Ensure().
		c.Assert(s.snapmgr.Ensure(), IsNil)
		s.state.Lock()
	}

	// Force another auto-refresh to check that notice is not repeated
	// for the same set of inhibited snaps.
	forceAutoRefresh()
	s.settle(c)

	chgs = s.state.Changes()
	c.Assert(chgs, HasLen, 2)
	// Old pre-download
	c.Check(chgs[0].Kind(), Equals, "pre-download")
	c.Check(chgs[0].Status(), Equals, state.DoneStatus)
	// New pre-download
	c.Check(chgs[1].Kind(), Equals, "pre-download")
	c.Check(chgs[1].Status(), Equals, state.DoneStatus)
	// No new notices recorded because the set of inhibited snaps was not changed.
	checkRefreshInhibitNotice(c, s.state, 1)
	checkLastRecordedInhibitedSnaps(c, s.state, []string{"some-snap"})

	// Add another inhibited snap.
	snapstate.Set(s.state, "some-other-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-other-snap", SnapID: "some-other-snap-id", Revision: snap.R(1)},
		}),
		Current:  snap.R(1),
		SnapType: string(snap.TypeApp),
	})

	// Force another auto-refresh to check that notice is recorded because
	// the set of inhibited snaps changed.
	forceAutoRefresh()
	s.settle(c)

	chgs = s.state.Changes()
	c.Assert(chgs, HasLen, 3)
	// Old pre-download
	c.Check(chgs[0].Kind(), Equals, "pre-download")
	c.Check(chgs[0].Status(), Equals, state.DoneStatus)
	// Old pre-download
	c.Check(chgs[1].Kind(), Equals, "pre-download")
	c.Check(chgs[1].Status(), Equals, state.DoneStatus)
	// Latest pre-download
	c.Check(chgs[1].Kind(), Equals, "pre-download")
	c.Check(chgs[1].Status(), Equals, state.DoneStatus)
	// New notice occurrence is recorded because the set of inhibited snaps changed.
	checkLastRecordedInhibitedSnaps(c, s.state, []string{"some-snap", "some-other-snap"})
	checkRefreshInhibitNotice(c, s.state, 2)
}

func checkLastRecordedInhibitedSnaps(c *C, st *state.State, snaps []string) {
	var lastRecordedInhibitedSnaps map[string]bool
	err := st.Get("last-recorded-inhibited-snaps", &lastRecordedInhibitedSnaps)
	if err != nil && !errors.Is(err, state.ErrNoState) {
		c.Fail()
	}
	c.Assert(len(lastRecordedInhibitedSnaps), Equals, len(snaps))
	for _, snap := range snaps {
		c.Assert(lastRecordedInhibitedSnaps[snap], Equals, true)
	}
}

func checkRefreshInhibitNotice(c *C, st *state.State, occurrences int) {
	notices := st.Notices(&state.NoticeFilter{Types: []state.NoticeType{state.RefreshInhibitNotice}})
	if occurrences == 0 {
		c.Assert(notices, HasLen, 0)
		return
	}
	c.Assert(notices, HasLen, 1)
	n := noticeToMap(c, notices[0])
	c.Check(n["type"], Equals, "refresh-inhibit")
	c.Check(n["key"], Equals, "-")
	c.Check(n["occurrences"], Equals, float64(occurrences))
	c.Check(n["user-id"], Equals, nil)
}

func (s *snapmgrTestSuite) TestAutoRefreshBusySnapButOngoingPreDownload(c *C) {
	restore := snapstate.MockRefreshAppsCheck(func(si *snap.Info) error {
		return snapstate.NewBusySnapError(si, []int{123}, nil, nil)
	})
	defer restore()

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

	si := &snap.SideInfo{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)}
	snapst := &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
		Current:  si.Revision,
		SnapType: string(snap.TypeApp),
	}
	snapstate.Set(s.state, "some-snap", snapst)
	snapsup := &snapstate.SnapSetup{
		SideInfo: &snap.SideInfo{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(2)},
		Flags:    snapstate.Flags{IsAutoRefresh: true},
	}

	// create ongoing pre-download
	chg := s.state.NewChange("pre-download", "")
	task := s.state.NewTask("pre-download-snap", "")
	task.Set("snap-setup", snapsup)
	task.Set("refresh-info", &userclient.PendingSnapRefreshInfo{InstanceName: "some-snap"})
	chg.AddTask(task)

	// don't create a pre-download task if one exists w/ these statuses
	for _, status := range []state.Status{state.DoStatus, state.DoingStatus} {
		task.SetStatus(status)
		_, err := snapstate.DoInstallOrPreDownload(s.state, snapst, snapsup, nil, snapstate.InstallContext{})

		var busyErr *snapstate.TimedBusySnapError
		c.Assert(errors.As(err, &busyErr), Equals, true)
		refreshInfo := busyErr.PendingSnapRefreshInfo()
		c.Check(refreshInfo, DeepEquals, &userclient.PendingSnapRefreshInfo{
			InstanceName:  "some-snap",
			TimeRemaining: snapstate.MaxInhibitionDuration(s.state),
		})

		// reset modified state to avoid conflicts
		snapst.RefreshInhibitedTime = nil
		snapstate.Set(s.state, "some-snap", snapst)
	}

	// a "Done" pre-download is ignored since the auto-refresh it causes might also be done
	task.SetStatus(state.DoneStatus)
	ts, err := snapstate.DoInstallOrPreDownload(s.state, snapst, snapsup, nil, snapstate.InstallContext{})
	c.Assert(err, FitsTypeOf, &snapstate.TimedBusySnapError{})
	c.Assert(ts.TaskSet().Tasks(), HasLen, 1)
}

func (s *snapmgrTestSuite) TestReRefreshCreatesPreDownloadChange(c *C) {
	s.o.TaskRunner().AddHandler("pre-download-snap", func(*state.Task, *tomb.Tomb) error { return nil }, nil)

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

	si := &snap.SideInfo{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)}
	snapst := &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
		Current:  si.Revision,
		SnapType: string(snap.TypeApp),
	}
	snapstate.Set(s.state, "some-snap", snapst)
	snapsup := &snapstate.SnapSetup{
		SideInfo: &snap.SideInfo{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(2)},
		Flags:    snapstate.Flags{IsAutoRefresh: true},
	}

	chg := s.state.NewChange("auto-refresh", "test change")
	// rerefresh looks for snaps by iterating through the other tasks in the change
	otherTask := s.state.NewTask("download-snap", "other test task")
	otherTask.Set("snap-setup", snapsup)
	otherTask.JoinLane(s.state.NewLane())
	otherTask.SetStatus(state.DoneStatus)
	otherTask.SetClean()

	chg.AddTask(otherTask)
	rerefreshTask := s.state.NewTask("check-rerefresh", "rerefresh task")
	rerefreshTask.Set("rerefresh-setup", snapstate.ReRefreshSetup{
		Flags: &snapstate.Flags{IsAutoRefresh: true},
	})
	chg.AddTask(rerefreshTask)

	restore := snapstate.MockReRefreshUpdateMany(func(context.Context, *state.State, []string, []*snapstate.RevisionOptions, int, snapstate.UpdateFilter, *snapstate.Flags, string) ([]string, *snapstate.UpdateTaskSets, error) {
		task := s.state.NewTask("test-pre-download", "test task")
		task.Set("snap-setup", snapsup)
		ts := state.NewTaskSet(task)
		return nil, &snapstate.UpdateTaskSets{PreDownload: []*state.TaskSet{ts}}, nil
	})
	defer restore()
	s.settle(c)

	chgs := s.state.Changes()
	// sort "auto-refresh" into first and "pre-download" into second
	sort.Slice(chgs, func(i, j int) bool {
		return chgs[i].Kind() < chgs[j].Kind()
	})

	c.Assert(chgs, HasLen, 2)
	c.Assert(chgs[0].Err(), IsNil)

	preDlChg := chgs[1]
	c.Assert(preDlChg.Err(), IsNil)
	c.Assert(preDlChg.Kind(), Equals, "pre-download")
	c.Assert(preDlChg.Summary(), Equals, "Pre-download \"some-snap\" for auto-refresh")
	c.Assert(preDlChg.Tasks(), HasLen, 1)
}

func (s *snapmgrTestSuite) TestDownloadTaskWaitsForPreDownload(c *C) {
	now := time.Now()
	restore := state.MockTime(now)
	defer restore()

	var notified bool
	restore = snapstate.MockAsyncPendingRefreshNotification(func(context.Context, *userclient.PendingSnapRefreshInfo) {
		notified = true
	})
	defer restore()

	var monitored bool
	restore = snapstate.MockCgroupMonitorSnapEnded(func(string, chan<- string) error {
		monitored = true
		return nil
	})
	defer restore()

	s.state.Lock()
	defer s.state.Unlock()
	preDlChg := s.state.NewChange("pre-download", "pre-download change")
	preDlTask := s.state.NewTask("pre-download-snap", "pre-download task")
	snapsup := &snapstate.SnapSetup{
		SideInfo: &snap.SideInfo{
			RealName: "foo",
			Revision: snap.R(2),
		},
		Flags: snapstate.Flags{IsAutoRefresh: true},
	}
	preDlTask.Set("snap-setup", snapsup)
	preDlTask.Set("refresh-info", &userclient.PendingSnapRefreshInfo{InstanceName: "foo"})
	preDlChg.AddTask(preDlTask)

	dlChg := s.state.NewChange("download", "download change")
	dlTask := s.state.NewTask("download-snap", "download task")
	dlTask.Set("snap-setup", snapsup)
	// wait until the pre-download is running
	dlTask.At(now.Add(time.Hour))
	dlChg.AddTask(dlTask)

	var downloadCalls int
	s.fakeStore.downloadCallback = func() {
		downloadCalls++

		switch downloadCalls {
		case 1:
			s.state.Lock()
			// schedule download to run while pre-download is running
			dlTask.At(time.Time{})
			s.state.Unlock()

			c.Assert(s.o.TaskRunner().Ensure(), IsNil)

			for i := 0; i < 5000; i++ {
				<-time.After(time.Millisecond)
				s.state.Lock()
				atTime := dlTask.AtTime()
				s.state.Unlock()
				if atTime.IsZero() {
					continue
				}

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

				// the download task registers itself w/ the pre-download and retries
				c.Assert(atTime.Equal(now.Add(2*time.Minute)), Equals, true)
				var taskIDs []string
				c.Assert(preDlTask.Get("waiting-tasks", &taskIDs), IsNil)
				c.Assert(taskIDs, DeepEquals, []string{dlTask.ID()})
				return
			}

			c.Fatal("download task hasn't run")
		case 2:
			return
		default:
			c.Fatal("only expected 2 calls to the store")
		}
	}

	s.settle(c)

	c.Assert(downloadCalls, Equals, 2)
	c.Assert(preDlTask.Status(), Equals, state.DoneStatus)
	c.Assert(dlTask.Status(), Equals, state.DoneStatus)
	c.Check(notified, Equals, false)
	c.Check(monitored, Equals, false)
}

func (s *snapmgrTestSuite) TestPreDownloadTaskContinuesAutoRefreshIfSoftCheckOk(c *C) {
	var softChecked bool
	restore := snapstate.MockRefreshAppsCheck(func(info *snap.Info) error {
		c.Assert(info.InstanceName(), Equals, "foo")
		softChecked = true
		return nil
	})
	defer restore()

	var notified bool
	restore = snapstate.MockAsyncPendingRefreshNotification(func(context.Context, *userclient.PendingSnapRefreshInfo) {
		notified = true
	})
	defer restore()

	var monitored bool
	restore = snapstate.MockCgroupMonitorSnapEnded(func(string, chan<- string) error {
		monitored = true
		return nil
	})
	defer restore()

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

	si := &snap.SideInfo{
		RealName: "foo",
		SnapID:   "foo-id",
		Revision: snap.R(1),
	}
	snaptest.MockSnap(c, `name: foo`, si)
	snapstate.Set(s.state, "foo", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
		Current:  si.Revision,
	})

	snapsup := &snapstate.SnapSetup{
		SideInfo: &snap.SideInfo{
			RealName: "foo",
			Revision: snap.R(2),
		},
		Flags: snapstate.Flags{IsAutoRefresh: true},
		// if there's no downloadInfo, the download goes into a fallback behaviour of requesting it from the store and
		// makes it harder to check that we don't request refresh info form the store
		DownloadInfo: &snap.DownloadInfo{DownloadURL: "my-url"},
	}
	s.state.Set("refresh-candidates", map[string]*snapstate.RefreshCandidate{
		"foo": {SnapSetup: *snapsup},
	})

	preDlChg := s.state.NewChange("pre-download", "pre-download change")
	preDlTask := s.state.NewTask("pre-download-snap", "pre-download task")

	preDlTask.Set("snap-setup", snapsup)
	preDlTask.Set("refresh-info", &userclient.PendingSnapRefreshInfo{InstanceName: "foo"})
	preDlChg.AddTask(preDlTask)

	s.settle(c)

	c.Assert(preDlTask.Status(), Equals, state.DoneStatus)

	c.Check(softChecked, Equals, true)
	c.Check(notified, Equals, false)
	c.Check(monitored, Equals, false)

	autoRefreshChg := findChange(s.state, "auto-refresh")
	c.Assert(autoRefreshChg, NotNil)
	c.Assert(autoRefreshChg.Status(), Equals, state.DoneStatus)

	// check that the auto-refresh was completed without asking the store for refresh info
	c.Assert(s.fakeBackend.ops.Count("storesvc-snap-action"), Equals, 0)
	c.Assert(s.fakeBackend.ops.Count("storesvc-download"), Equals, 2)
	c.Assert(s.fakeStore.downloads, DeepEquals, []fakeDownload{
		{
			name:   "foo",
			target: filepath.Join(dirs.SnapBlobDir, "foo_2.snap"),
			opts: &store.DownloadOptions{
				Scheduled:           true,
				LeavePartialOnError: true,
			},
		},
		{
			name:   "foo",
			target: filepath.Join(dirs.SnapBlobDir, "foo_2.snap"),
			opts: &store.DownloadOptions{
				Scheduled:           true,
				LeavePartialOnError: true,
			},
		},
	})
}

func findChange(st *state.State, kind string) *state.Change {
	for _, chg := range st.Changes() {
		if chg.Kind() == kind {
			return chg
		}
	}

	return nil
}

func (s *snapmgrTestSuite) TestDownloadTaskMonitorsSnapStoppedAndNotifiesOnSoftCheckFail(c *C) {
	s.state.Lock()
	si := &snap.SideInfo{
		RealName: "foo",
		SnapID:   "foo-id",
		Revision: snap.R(1),
	}
	snaptest.MockSnap(c, `name: foo`, si)
	snapstate.Set(s.state, "foo", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
		Current:  si.Revision,
	})
	snapsup := &snapstate.SnapSetup{
		SideInfo: &snap.SideInfo{
			RealName: "foo",
			Revision: snap.R(2),
		},
		Flags: snapstate.Flags{IsAutoRefresh: true},
		// if there's no downloadInfo, the download goes into a fallback behaviour of requesting it from the store and
		// makes it harder to check that we don't request refresh info form the store
		DownloadInfo: &snap.DownloadInfo{DownloadURL: "my-url"},
	}
	s.state.Set("refresh-candidates", map[string]*snapstate.RefreshCandidate{
		"foo": {SnapSetup: *snapsup},
	})

	var softChecked bool
	inhibited := true
	restore := snapstate.MockRefreshAppsCheck(func(info *snap.Info) error {
		c.Assert(info.InstanceName(), Equals, "foo")
		softChecked = true
		if inhibited {
			return snapstate.NewBusySnapError(info, []int{123}, nil, nil)
		}
		return nil
	})
	defer restore()

	var notified bool
	restore = snapstate.MockAsyncPendingRefreshNotification(func(context.Context, *userclient.PendingSnapRefreshInfo) {
		notified = true
	})
	defer restore()

	var monitorSignal chan<- string
	restore = snapstate.MockCgroupMonitorSnapEnded(func(name string, done chan<- string) error {
		c.Assert(name, Equals, "foo")
		monitorSignal = done
		return nil
	})
	defer restore()

	preDlChg := s.state.NewChange("pre-download", "pre-download change")
	preDlTask := s.state.NewTask("pre-download-snap", "pre-download task")

	preDlTask.Set("snap-setup", snapsup)
	preDlTask.Set("refresh-info", &userclient.PendingSnapRefreshInfo{InstanceName: "foo"})
	preDlChg.AddTask(preDlTask)

	s.settle(c)

	c.Assert(preDlTask.Status(), Equals, state.DoneStatus)
	c.Assert(s.fakeStore.downloads, HasLen, 1)
	c.Check(s.fakeStore.downloads[0].name, Equals, "foo")

	// the soft check failed so we notified and started monitoring
	c.Check(softChecked, Equals, true)
	c.Check(notified, Equals, true)
	c.Assert(monitorSignal, NotNil)

	var hints map[string]*snapstate.RefreshCandidate
	err := s.state.Get("refresh-candidates", &hints)
	c.Assert(err, IsNil)
	c.Assert(hints, HasLen, 1)
	c.Assert(hints["foo"].Monitored, Equals, true)

	monitored := s.state.Cached("monitored-snaps")
	c.Assert(monitored, FitsTypeOf, map[string]context.CancelFunc{})
	c.Assert(monitored.(map[string]context.CancelFunc)["foo"], NotNil)

	// signal snap has stopped and wait for pending goroutine to finish
	s.state.Unlock()
	inhibited = false
	monitorSignal <- "foo"

	waitForMonitoringEnd(s.state, c)

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

	autoRefreshChg := findChange(s.state, "auto-refresh")
	c.Assert(autoRefreshChg, NotNil)
	c.Assert(autoRefreshChg.Status(), Equals, state.DoneStatus)
	c.Assert(s.state.Cached("monitored-snaps"), IsNil)

	// the refresh-candidates are removed at the end of the change (see pruneRefreshCandidates)
	err = s.state.Get("refresh-candidates", &hints)
	c.Assert(err, testutil.ErrorIs, &state.NoStateError{})

	// check that the auto-refresh was completed without asking the store for refresh info
	c.Assert(s.fakeBackend.ops.Count("storesvc-snap-action"), Equals, 0)
	c.Assert(s.fakeStore.downloads, HasLen, 2)
}

func (s *snapmgrTestSuite) TestDownloadTaskMonitorsRepeated(c *C) {
	s.state.Lock()
	defer s.state.Unlock()
	si := &snap.SideInfo{
		RealName: "foo",
		SnapID:   "foo-id",
		Revision: snap.R(1),
	}
	snaptest.MockSnap(c, `name: foo`, si)
	snapstate.Set(s.state, "foo", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
		Current:  si.Revision,
	})
	snapsup := &snapstate.SnapSetup{
		SideInfo: &snap.SideInfo{
			RealName: "foo",
			Revision: snap.R(2),
		},
		Flags: snapstate.Flags{IsAutoRefresh: true},
	}
	s.state.Set("refresh-candidates", map[string]*snapstate.RefreshCandidate{
		"foo": {SnapSetup: *snapsup},
	})

	var softChecked bool
	inhibited := true
	restore := snapstate.MockRefreshAppsCheck(func(info *snap.Info) error {
		c.Assert(info.InstanceName(), Equals, "foo")
		softChecked = true
		if inhibited {
			return snapstate.NewBusySnapError(info, []int{123}, nil, nil)
		}
		return nil
	})
	defer restore()

	var notified bool
	restore = snapstate.MockAsyncPendingRefreshNotification(func(context.Context, *userclient.PendingSnapRefreshInfo) {
		notified = true
	})
	defer restore()

	var monitorSignal chan<- string
	restore = snapstate.MockCgroupMonitorSnapEnded(func(name string, done chan<- string) error {
		c.Assert(name, Equals, "foo")
		monitorSignal = done
		return nil
	})
	defer restore()

	preDlChg := s.state.NewChange("pre-download", "pre-download change")
	preDlTask := s.state.NewTask("pre-download-snap", "pre-download task")

	preDlTask.Set("snap-setup", snapsup)
	preDlTask.Set("refresh-info", &userclient.PendingSnapRefreshInfo{InstanceName: "foo"})
	preDlChg.AddTask(preDlTask)

	s.settle(c)

	c.Assert(preDlTask.Status(), Equals, state.DoneStatus)
	// monitoring snap
	monitored := s.state.Cached("monitored-snaps")
	c.Assert(monitored, FitsTypeOf, map[string]context.CancelFunc{})
	c.Assert(monitored.(map[string]context.CancelFunc)["foo"], NotNil)
	c.Assert(notified, Equals, true)

	// waiting for the monitoring to end
	c.Check(s.state.Cached("monitored-snaps"), NotNil)
	c.Assert(findChange(s.state, "auto-refresh"), IsNil)

	// start a new pre-download which shouldn't start monitoring
	preDlChg = s.state.NewChange("pre-download", "pre-download change")
	preDlTask = s.state.NewTask("pre-download-snap", "pre-download task")
	preDlTask.Set("snap-setup", snapsup)
	preDlTask.Set("refresh-info", &userclient.PendingSnapRefreshInfo{InstanceName: "foo"})
	preDlChg.AddTask(preDlTask)

	// reset the watcher variables
	notified = false
	softChecked = false
	firstMonitorSignal := monitorSignal
	monitorSignal = nil

	s.settle(c)

	c.Check(softChecked, Equals, true)
	c.Check(notified, Equals, true)
	// didn't wait for snap to stop because there's already a goroutine doing it
	c.Check(monitorSignal, IsNil)

	// make sure nothing is left running
	s.state.Unlock()
	inhibited = false
	firstMonitorSignal <- "foo"
	waitForMonitoringEnd(s.state, c)
	s.state.Lock()

	s.settle(c)
}

func (s *snapmgrTestSuite) TestUnlinkMonitorSnapOnHardCheckFailure(c *C) {
	s.state.Lock()
	defer s.state.Unlock()
	si := &snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Revision: snap.R(1),
	}
	snaptest.MockSnap(c, `name: some-snap`, si)
	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
		Current:  si.Revision,
	})

	s.fakeStore.downloads = []fakeDownload{{
		macaroon: s.user.StoreMacaroon,
		name:     "some-snap",
		target:   filepath.Join(dirs.SnapBlobDir, "some-snap_instance_11.snap"),
	}}

	var notified bool
	restore := snapstate.MockAsyncPendingRefreshNotification(func(_ context.Context, pendingInfo *userclient.PendingSnapRefreshInfo) {
		c.Check(pendingInfo.InstanceName, Equals, "some-snap")
		c.Check(pendingInfo.TimeRemaining, Equals, snapstate.MaxInhibitionDuration(s.state))
		notified = true
	})
	defer restore()

	var monitorSignal chan<- string
	restore = snapstate.MockCgroupMonitorSnapEnded(func(name string, done chan<- string) error {
		c.Check(name, Equals, "some-snap")
		monitorSignal = done
		return nil
	})
	defer restore()

	var check int
	restore = snapstate.MockRefreshAppsCheck(func(info *snap.Info) error {
		check++
		c.Check(info.InstanceName(), Equals, "some-snap")

		switch check {
		case 1:
			return nil
		case 2:
			return snapstate.NewBusySnapError(info, []int{123}, nil, nil)
		default:
			return nil
		}
	})
	defer restore()

	updated, tss, err := snapstate.AutoRefresh(context.Background(), s.state)
	c.Assert(err, IsNil)
	c.Check(updated, DeepEquals, []string{"some-snap"})
	c.Assert(tss, NotNil)
	c.Check(tss.Refresh, NotNil)
	c.Check(tss.PreDownload, IsNil)

	chg := s.state.NewChange("refresh", "test refresh")
	for _, ts := range tss.Refresh {
		chg.AddAll(ts)
	}

	s.settle(c)
	c.Assert(chg.Status(), Equals, state.ErrorStatus)

	c.Check(notified, Equals, true)
	c.Check(check, Equals, 2)
	c.Check(monitorSignal, NotNil)

	monitored := s.state.Cached("monitored-snaps")
	c.Assert(monitored, FitsTypeOf, map[string]context.CancelFunc{})
	c.Assert(monitored.(map[string]context.CancelFunc)["some-snap"], NotNil)

	// cleanup leftover routines
	s.state.Unlock()
	monitorSignal <- "some-snap"
	waitForMonitoringEnd(s.state, c)

	s.state.Lock()
	s.settle(c)
}

func (s *snapmgrTestSuite) TestRefreshForcedOnRefreshInhibitionTimeout(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	instant := time.Now()
	pastInstant := instant.Add(-snapstate.MaxInhibitionDuration(s.state) * 2)
	// Add first snap
	si1 := &snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Revision: snap.R(1),
	}
	snaptest.MockSnap(c, `name: some-snap`, si1)
	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si1}),
		Current:  si1.Revision,
		// Pretend inhibition is overdue.
		RefreshInhibitedTime: &pastInstant,
	})
	// Add second snap to check that the list is being appended to properly.
	si2 := &snap.SideInfo{
		RealName: "some-other-snap",
		SnapID:   "some-other-snap-id",
		Revision: snap.R(1),
	}
	snaptest.MockSnap(c, `name: some-other-snap`, si2)
	snapstate.Set(s.state, "some-other-snap", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si2}),
		Current:  si2.Revision,
		// Pretend inhibition is overdue.
		RefreshInhibitedTime: &pastInstant,
	})

	s.fakeStore.downloads = []fakeDownload{
		{
			macaroon: s.user.StoreMacaroon,
			name:     "some-snap",
			target:   filepath.Join(dirs.SnapBlobDir, "some-snap_instance_11.snap"),
		},
		{
			macaroon: s.user.StoreMacaroon,
			name:     "some-other-snap",
			target:   filepath.Join(dirs.SnapBlobDir, "some-other-snap_instance_11.snap"),
		},
	}

	var notified int
	restore := snapstate.MockAsyncPendingRefreshNotification(func(_ context.Context, pendingInfo *userclient.PendingSnapRefreshInfo) {
		c.Check(pendingInfo.TimeRemaining, Equals, time.Duration(0))
		notified++
	})
	defer restore()

	check := make(map[string]int, 2)
	restore = snapstate.MockRefreshAppsCheck(func(info *snap.Info) error {
		check[info.InstanceName()]++

		switch check[info.InstanceName()] {
		case 1:
			return nil
		case 2:
			return snapstate.NewBusySnapError(info, []int{123}, nil, nil)
		default:
			return nil
		}
	})
	defer restore()

	updated, tss, err := snapstate.AutoRefresh(context.Background(), s.state)
	c.Assert(err, IsNil)
	sort.Slice(updated, func(i, j int) bool {
		return updated[i] < updated[j]
	})
	c.Check(updated, DeepEquals, []string{"some-other-snap", "some-snap"})
	c.Assert(tss, NotNil)
	c.Check(tss.Refresh, NotNil)
	c.Check(tss.PreDownload, IsNil)

	chg := s.state.NewChange("refresh", "test refresh")
	for _, ts := range tss.Refresh {
		chg.AddAll(ts)
	}

	s.settle(c)
	c.Assert(chg.Status(), Equals, state.DoneStatus)

	var apiData map[string]any
	c.Assert(chg.Get("api-data", &apiData), IsNil)
	refreshForced := apiData["refresh-forced"].([]any)
	sort.Slice(refreshForced, func(i, j int) bool {
		return refreshForced[i].(string) < refreshForced[j].(string)
	})
	c.Check(refreshForced, DeepEquals, []any{"some-other-snap", "some-snap"})

	notices := s.state.Notices(&state.NoticeFilter{Types: []state.NoticeType{state.ChangeUpdateNotice}})
	c.Assert(notices, HasLen, 1)
	n := noticeToMap(c, notices[0])
	c.Check(n["type"], Equals, "change-update")
	c.Check(n["key"], Equals, chg.ID())
	// 3 status changes (Default -> Doing -> Done) + 2 forced refreshes
	c.Check(n["occurrences"], Equals, 5.0)

	c.Check(notified, Equals, 2)
	c.Check(check["some-snap"], Equals, 2)
	c.Check(check["some-other-snap"], Equals, 2)
}

func (s *snapmgrTestSuite) TestRefreshForcedOnRefreshInhibitionTimeoutError(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	instant := time.Now()
	pastInstant := instant.Add(-snapstate.MaxInhibitionDuration(s.state) * 2)
	// Add snap
	si1 := &snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Revision: snap.R(1),
	}
	snaptest.MockSnap(c, `name: some-snap`, si1)
	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si1}),
		Current:  si1.Revision,
		// Pretend inhibition is overdue.
		RefreshInhibitedTime: &pastInstant,
	})

	s.fakeStore.downloads = []fakeDownload{
		{
			macaroon: s.user.StoreMacaroon,
			name:     "some-snap",
			target:   filepath.Join(dirs.SnapBlobDir, "some-snap_instance_11.snap"),
		},
	}

	restore := snapstate.MockAsyncPendingRefreshNotification(func(_ context.Context, pendingInfo *userclient.PendingSnapRefreshInfo) {})
	defer restore()

	var checkAppRunning int
	restore = snapstate.MockRefreshAppsCheck(func(info *snap.Info) error {
		checkAppRunning++
		// pass on soft-check, fail on hard-check in unlink-current-snap
		if checkAppRunning > 1 {
			return snapstate.NewBusySnapError(info, []int{123}, nil, nil)
		}
		return nil
	})
	defer restore()

	updated, tss, err := snapstate.AutoRefresh(context.Background(), s.state)
	c.Assert(err, IsNil)
	c.Check(updated, DeepEquals, []string{"some-snap"})
	c.Assert(tss, NotNil)
	c.Check(tss.Refresh, NotNil)
	c.Check(tss.PreDownload, IsNil)

	chg := s.state.NewChange("refresh", "test refresh")
	for _, ts := range tss.Refresh {
		chg.AddAll(ts)
	}

	restore = snapstate.MockOnRefreshInhibitionTimeout(func(chg *state.Change, snapName string) error {
		return fmt.Errorf("boom!")
	})
	defer restore()

	s.settle(c)
	c.Assert(chg.Status(), Equals, state.ErrorStatus)
	c.Assert(checkAppRunning, Equals, 2)

	lock, err := osutil.NewFileLock(filepath.Join(s.fakeBackend.lockDir, "some-snap.lock"))
	c.Assert(err, IsNil)
	defer lock.Close()
	c.Assert(lock.TryLock(), IsNil)
}

func (s *snapmgrTestSuite) TestDeletedMonitoredMapIsCorrectlyDeleted(c *C) {
	s.state.Lock()
	si := &snap.SideInfo{
		RealName: "foo",
		SnapID:   "foo-id",
		Revision: snap.R(1),
	}
	snaptest.MockSnap(c, `name: foo`, si)
	snapstate.Set(s.state, "foo", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
		Current:  si.Revision,
	})
	snapsup := &snapstate.SnapSetup{
		SideInfo: &snap.SideInfo{
			RealName: "foo",
			Revision: snap.R(2),
		},
		Flags: snapstate.Flags{IsAutoRefresh: true},
	}
	s.state.Set("refresh-candidates", map[string]*snapstate.RefreshCandidate{
		"foo": {SnapSetup: *snapsup},
	})

	inhibited := true
	restore := snapstate.MockRefreshAppsCheck(func(info *snap.Info) error {
		if inhibited {
			return snapstate.NewBusySnapError(info, []int{123}, nil, nil)
		}
		return nil
	})
	defer restore()

	var monitorSignal chan<- string
	restore = snapstate.MockCgroupMonitorSnapEnded(func(name string, done chan<- string) error {
		c.Check(name, Equals, "foo")
		monitorSignal = done
		return nil
	})
	defer restore()

	preDlChg := s.state.NewChange("pre-download", "pre-download change")
	preDlTask := s.state.NewTask("pre-download-snap", "pre-download task")

	preDlTask.Set("snap-setup", snapsup)
	preDlTask.Set("refresh-info", &userclient.PendingSnapRefreshInfo{InstanceName: "foo"})
	preDlChg.AddTask(preDlTask)

	s.settle(c)
	c.Assert(preDlChg.Status(), Equals, state.DoneStatus)

	// unblock the monitoring routine which will delete the "monitored-snaps" map
	s.state.Unlock()

	// let the continuing logic create an auto-refresh change
	inhibited = false
	monitorSignal <- "foo"
	waitForMonitoringEnd(s.state, c)

	s.state.Lock()
	c.Assert(s.state.Cached("monitored-snaps"), IsNil)

	// start a 2nd task that checks the state for the map of monitored snap
	preDlChg = s.state.NewChange("pre-download", "pre-download change")
	preDlTask = s.state.NewTask("pre-download-snap", "pre-download task")
	preDlTask.Set("snap-setup", snapsup)
	preDlTask.Set("refresh-info", &userclient.PendingSnapRefreshInfo{InstanceName: "foo"})
	preDlChg.AddTask(preDlTask)

	// so we go into the monitoring
	inhibited = true

	s.settle(c)
	c.Assert(preDlChg.Status(), Equals, state.DoneStatus)

	// wait until the 2nd auto-refresh starts
	s.state.Unlock()
	inhibited = false
	monitorSignal <- "foo"
	waitFor(s.state, c, func() bool { return s.state.Change("4") != nil })
	s.state.Lock()
	defer s.state.Unlock()
	c.Assert(s.state.Change("4").Kind(), Equals, "auto-refresh")
	s.settle(c)

	c.Assert(s.state.Cached("monitored-snaps"), IsNil)
}

func (s *snapmgrTestSuite) TestDeletedMonitoredMapIsCorrectlyDeletedAfterRefreshCandidateChange(c *C) {
	s.state.Lock()
	si := &snap.SideInfo{
		RealName: "foo",
		SnapID:   "foo-id",
		Revision: snap.R(1),
	}
	snaptest.MockSnap(c, `name: foo`, si)
	snapstate.Set(s.state, "foo", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
		Current:  si.Revision,
	})
	snapsup := &snapstate.SnapSetup{
		SideInfo: &snap.SideInfo{
			RealName: "foo",
			Revision: snap.R(2),
		},
		Flags: snapstate.Flags{IsAutoRefresh: true},
	}
	s.state.Set("refresh-candidates", map[string]*snapstate.RefreshCandidate{
		"foo": {SnapSetup: *snapsup},
	})

	restore := snapstate.MockRefreshAppsCheck(func(info *snap.Info) error {
		return snapstate.NewBusySnapError(info, []int{123}, nil, nil)
	})
	defer restore()

	restore = snapstate.MockCgroupMonitorSnapEnded(func(name string, done chan<- string) error {
		return nil
	})
	defer restore()

	preDlChg := s.state.NewChange("pre-download", "pre-download change")
	preDlTask := s.state.NewTask("pre-download-snap", "pre-download task")

	preDlTask.Set("snap-setup", snapsup)
	preDlTask.Set("refresh-info", &userclient.PendingSnapRefreshInfo{InstanceName: "foo"})
	preDlChg.AddTask(preDlTask)

	s.settle(c)
	c.Assert(preDlChg.Status(), Equals, state.DoneStatus)

	// snap is monitored for refresh
	monitored := s.state.Cached("monitored-snaps").(map[string]context.CancelFunc)
	abortMonitored := monitored["foo"]
	c.Assert(monitored["foo"], NotNil)
	// Abort and remove from refresh candidates while still holding state lock
	abortMonitored()
	s.state.Set("refresh-candidates", nil)

	// unblock the monitoring routine which should delete the "monitored-snaps" map
	// even when corresponding refresh candidate is not available.
	s.state.Unlock()

	// wait for monitored snaps to be cleared
	waitFor(s.state, c, func() bool {
		s.settle(c)
		return s.state.Cached("monitored-snaps") == nil
	})

	s.state.Lock()
	// monitoring removal is robust and doesn't fail when corresponding
	// refresh candidate is not available.
	c.Assert(s.state.Cached("monitored-snaps"), IsNil)
	s.state.Unlock()
}

func waitFor(st *state.State, c *C, cond func() bool) {
	for i := 0; i < 5000; i++ {
		st.Lock()
		condMet := cond()
		st.Unlock()
		if condMet {
			return
		}

		<-time.After(time.Millisecond)
	}

	c.Fatal("condition wasn't met within 5 seconds")
}

func waitForMonitoringEnd(st *state.State, c *C) {
	waitFor(st, c, func() bool {
		return findChange(st, "auto-refresh") != nil
	})
}

func (s *snapmgrTestSuite) TestPreDownloadWithIgnoreRunningRefresh(c *C) {
	s.state.Lock()
	defer s.state.Unlock()
	si := &snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Revision: snap.R(1),
	}
	snaptest.MockSnap(c, `name: some-snap`, si)
	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
		Current:  si.Revision,
	})
	snapsup := &snapstate.SnapSetup{
		SideInfo: &snap.SideInfo{
			RealName: "some-snap",
			Revision: snap.R(2),
		},
		Flags: snapstate.Flags{IsAutoRefresh: true},
	}
	s.state.Set("refresh-candidates", map[string]*snapstate.RefreshCandidate{
		"some-snap": {SnapSetup: *snapsup},
	})

	restore := snapstate.MockAsyncPendingRefreshNotification(func(ctx context.Context, refreshInfo *userclient.PendingSnapRefreshInfo) {})
	defer restore()

	restore = snapstate.MockRefreshAppsCheck(func(info *snap.Info) error {
		c.Assert(info.InstanceName(), Equals, "some-snap")
		return snapstate.NewBusySnapError(info, []int{123}, nil, nil)
	})
	defer restore()

	var monitorSignal chan<- string
	restore = snapstate.MockCgroupMonitorSnapEnded(func(name string, done chan<- string) error {
		c.Check(name, Equals, "some-snap")
		monitorSignal = done
		return nil
	})
	defer restore()

	preDlChg := s.state.NewChange("pre-download", "pre-download change")
	preDlTask := s.state.NewTask("pre-download-snap", "pre-download task")

	preDlTask.Set("snap-setup", snapsup)
	preDlTask.Set("refresh-info", &userclient.PendingSnapRefreshInfo{InstanceName: "some-snap"})
	preDlChg.AddTask(preDlTask)

	s.settle(c)

	c.Assert(preDlTask.Status(), Equals, state.DoneStatus)
	// check there's still a goroutine monitoring the snap
	monitored := s.state.Cached("monitored-snaps")
	c.Assert(monitored, FitsTypeOf, map[string]context.CancelFunc{})
	c.Assert(monitored.(map[string]context.CancelFunc)["some-snap"], NotNil)

	updated, tss, err := snapstate.UpdateMany(context.Background(), s.state, []string{"some-snap"}, nil, 0, &snapstate.Flags{IgnoreRunning: true})
	c.Assert(err, IsNil)
	c.Assert(updated, DeepEquals, []string{"some-snap"})
	c.Assert(tss, NotNil)

	chg := s.state.NewChange("refresh", "refresh change")
	for _, ts := range tss {
		chg.AddAll(ts)
	}
	s.settle(c)

	c.Assert(chg.Status(), Equals, state.DoneStatus)
	c.Assert(chg.Err(), IsNil)

	// wait for the monitoring to be cleared
	for i := 0; i < 1000; i++ {
		s.state.Unlock()
		<-time.After(1 * time.Millisecond)
		s.state.Lock()

		// the monitoring has stopped but no auto-refresh was or will be attempted
		if monitored := s.state.Cached("monitored-snaps"); monitored == nil {
			break
		}
	}

	// the monitoring has stopped but no auto-refresh was or will be attempted
	c.Check(s.state.Cached("monitored-snaps"), IsNil, Commentf("monitoring wasn't cleared after 10 seconds"))
	c.Check(s.state.Cached("auto-refresh-continue-attempt"), IsNil)
	lastRefresh, err := s.snapmgr.LastRefresh()
	c.Assert(err, IsNil)
	c.Check(lastRefresh.IsZero(), Equals, true)

	// if the snap stops, we don't block when using the channel to notify
	monitorSignal <- "some-snap"
}

func (s *snapmgrTestSuite) TestPreDownloadCleansSnapDownloads(c *C) {
	s.state.Lock()
	defer s.state.Unlock()
	si := &snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Revision: snap.R(1),
	}
	snaptest.MockSnap(c, `name: some-snap`, si)
	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active: true,
		Sequence: sequence.SnapSequence{
			Revisions: []*sequence.RevisionSideState{{Snap: si}},
		},
		Current: si.Revision,
	})
	snapsup := &snapstate.SnapSetup{
		SideInfo: &snap.SideInfo{
			RealName: "some-snap",
			Revision: snap.R(2),
		},
		Flags: snapstate.Flags{IsAutoRefresh: true},
	}
	s.state.Set("refresh-candidates", map[string]*snapstate.RefreshCandidate{
		"some-snap": {SnapSetup: *snapsup},
	})

	restore := snapstate.MockAsyncPendingRefreshNotification(func(ctx context.Context, refreshInfo *userclient.PendingSnapRefreshInfo) {})
	defer restore()

	restore = snapstate.MockRefreshAppsCheck(func(info *snap.Info) error {
		c.Assert(info.InstanceName(), Equals, "some-snap")
		return snapstate.NewBusySnapError(info, []int{123}, nil, nil)
	})
	defer restore()

	// mock that snap is monitored (i.e. non-nil abort channel)
	mockAbortChans := map[string]any{"some-snap": func() {}}
	s.state.Cache("monitored-snaps", mockAbortChans)

	preDlChg := s.state.NewChange("pre-download", "pre-download change")
	preDlTask := s.state.NewTask("pre-download-snap", "pre-download task")

	preDlTask.Set("snap-setup", snapsup)
	preDlTask.Set("refresh-info", &userclient.PendingSnapRefreshInfo{InstanceName: "some-snap"})
	preDlChg.AddTask(preDlTask)

	cleanSnapDownloadsCalled := false
	restore = snapstate.MockCleanSnapDownloads(func(st *state.State, snapName string) error {
		if snapName == "some-snap" {
			cleanSnapDownloadsCalled = true
		}
		return nil
	})
	defer restore()

	s.settle(c)
	c.Check(cleanSnapDownloadsCalled, Equals, true)
}

func (s *snapmgrTestSuite) TestRefreshNoRelatedMonitoring(c *C) {
	s.state.Lock()
	defer s.state.Unlock()
	si := &snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Revision: snap.R(1),
	}
	snaptest.MockSnap(c, `name: some-snap`, si)
	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
		Current:  si.Revision,
	})
	s.state.Cache("monitored-snaps", map[string]context.CancelFunc{"other-snap": func() {}})

	_, tss, err := snapstate.UpdateMany(context.Background(), s.state, []string{"some-snap"}, nil, 0, &snapstate.Flags{IgnoreRunning: true})
	c.Assert(err, IsNil)

	chg := s.state.NewChange("refresh", "refresh change")
	for _, ts := range tss {
		chg.AddAll(ts)
	}
	s.settle(c)

	c.Assert(chg.Status(), Equals, state.DoneStatus)
}

func (s *snapmgrTestSuite) TestMonitoringIsPersistedAndRestored(c *C) {
	s.state.Lock()
	defer s.state.Unlock()
	si := &snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Revision: snap.R(1),
	}
	snaptest.MockSnap(c, `name: some-snap`, si)
	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
		Current:  si.Revision,
	})
	snapsup := &snapstate.SnapSetup{
		SideInfo: &snap.SideInfo{
			RealName: "some-snap",
			Revision: snap.R(2),
		},
		Flags: snapstate.Flags{IsAutoRefresh: true},
	}
	// simulate a restart with an in-progress monitoring
	s.state.Set("refresh-candidates", map[string]*snapstate.RefreshCandidate{
		"some-snap": {SnapSetup: *snapsup, Monitored: true},
	})

	var notified bool
	restore := snapstate.MockAsyncPendingRefreshNotification(func(ctx context.Context, refreshInfo *userclient.PendingSnapRefreshInfo) {})
	defer restore()

	restore = snapstate.MockRefreshAppsCheck(func(info *snap.Info) error {
		return nil
	})
	defer restore()

	var stopMonitor chan<- string
	restore = snapstate.MockCgroupMonitorSnapEnded(func(name string, done chan<- string) error {
		stopMonitor = done
		c.Check(name, Equals, "some-snap")
		return nil
	})
	defer restore()

	s.state.Unlock()
	defer s.state.Lock()
	af := snapstate.NewAutoRefresh(s.state)
	err := af.Ensure()
	c.Check(err, IsNil)

	// restores monitoring but doesn't notify again
	c.Assert(stopMonitor, NotNil)
	c.Assert(notified, Equals, false)

	s.state.Lock()
	aborts := s.state.Cached("monitored-snaps").(map[string]context.CancelFunc)
	abort := aborts["some-snap"]
	s.state.Unlock()
	c.Assert(abort, NotNil)

	stopMonitor <- "some-snap"
	waitForMonitoringEnd(s.state, c)

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

	// the refresh-candidates are removed at the end of the change (see pruneRefreshCandidates)
	var hints map[string]*snapstate.RefreshCandidate
	err = s.state.Get("refresh-candidates", &hints)
	c.Assert(err, testutil.ErrorIs, &state.NoStateError{})
}

func (s *snapmgrTestSuite) TestNoMonitoringIfOnlyOtherRefreshCandidates(c *C) {
	s.testNoMonitoringWithCands(c, map[string]*snapstate.RefreshCandidate{
		"other-snap": {
			SnapSetup: snapstate.SnapSetup{
				SideInfo: &snap.SideInfo{
					Revision: snap.R(11),
				},
			},
		},
	})
}

func (s *snapmgrTestSuite) TestNoMonitoringIfNoRefreshCandidates(c *C) {
	s.testNoMonitoringWithCands(c, nil)
}

func (s *snapmgrTestSuite) testNoMonitoringWithCands(c *C, cands map[string]*snapstate.RefreshCandidate) {
	s.state.Lock()
	defer s.state.Unlock()
	si := &snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Revision: snap.R(1),
	}
	snaptest.MockSnap(c, `name: some-snap`, si)
	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
		Current:  si.Revision,
	})
	snapsup := &snapstate.SnapSetup{
		SideInfo: &snap.SideInfo{
			RealName: "some-snap",
			Revision: snap.R(2),
		},
		Flags: snapstate.Flags{IsAutoRefresh: true},
	}

	// cands shouldn't include a refresh candidate for this snap so we can simulate
	// that the candidate was reverted before the pre-download task runs
	s.state.Set("refresh-candidates", cands)

	var notified bool
	restore := snapstate.MockAsyncPendingRefreshNotification(func(ctx context.Context, refreshInfo *userclient.PendingSnapRefreshInfo) {
		notified = true
	})
	defer restore()

	var inhibited bool
	restore = snapstate.MockRefreshAppsCheck(func(info *snap.Info) error {
		inhibited = true
		return snapstate.NewBusySnapError(info, []int{123}, nil, nil)
	})
	defer restore()

	restore = snapstate.MockCgroupMonitorSnapEnded(func(name string, done chan<- string) error {
		return nil
	})
	defer restore()

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

	preDlChg := s.state.NewChange("pre-download", "pre-download change")
	preDlTask := s.state.NewTask("pre-download-snap", "pre-download task")

	preDlTask.Set("snap-setup", snapsup)
	preDlTask.Set("refresh-info", &userclient.PendingSnapRefreshInfo{InstanceName: "some-snap"})
	preDlChg.AddTask(preDlTask)

	s.settle(c)

	// task finished without waiting for monitoring
	c.Assert(preDlTask.Status(), Equals, state.DoneStatus)
	c.Assert(s.state.Cached("monitored-snap"), IsNil)
	c.Assert(buf.String(), testutil.Contains, `cannot get refresh candidate for "some-snap" (possibly reverted): nothing to refresh`)

	// we didn't notify since there's no candidate to refresh to
	c.Assert(notified, Equals, false)
	c.Assert(inhibited, Equals, true)
}

func (s *snapmgrTestSuite) testUpdateDowngradeBlockedByOtherChanges(old, new string, revert bool) error {
	si1 := snap.SideInfo{
		RealName: "snapd",
		SnapID:   "snapd-id",
		Channel:  "latest",
		Revision: snap.R(1),
	}
	si2 := snap.SideInfo{
		RealName: "snapd",
		SnapID:   "snapd-id",
		Channel:  "latest",
		Revision: snap.R(2),
	}
	si3 := snap.SideInfo{
		RealName: "snapd",
		SnapID:   "snapd-id",
		Channel:  "latest",
		Revision: snap.R(3),
	}

	restore := snapstate.MockSnapReadInfo(func(name string, si *snap.SideInfo) (*snap.Info, error) {
		var version string
		switch name {
		case "snapd":
			if (revert && si.Revision.N == 1) || (!revert && si.Revision.N == 2) {
				version = old
			} else if (revert && si.Revision.N == 2) || si.Revision.N == 3 {
				version = new
			} else {
				return nil, fmt.Errorf("unexpected revision for test")
			}
		default:
			version = "1.0"
		}
		return &snap.Info{
			SuggestedName: name,
			Version:       version,
			Architectures: []string{"all"},
			SideInfo:      *si,
		}, nil
	})
	defer restore()

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

	chg := st.NewChange("unrelated", "...")
	chg.AddTask(st.NewTask("task0", "..."))

	snapstate.Set(s.state, "snapd", &snapstate.SnapState{
		Active:          true,
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si1, &si2, &si3}),
		TrackingChannel: "latest/stable",
		Current:         si2.Revision,
	})

	var err error
	if revert {
		_, err = snapstate.Revert(s.state, "snapd", snapstate.Flags{}, "")
	} else {
		_, err = snapstate.Update(s.state, "snapd", &snapstate.RevisionOptions{Revision: snap.R(3)}, s.user.ID, snapstate.Flags{})
	}
	return err
}

func (s *snapmgrTestSuite) TestUpdateDowngradeBlockedByOtherChanges(c *C) {
	err := s.testUpdateDowngradeBlockedByOtherChanges("2.57.1", "2.56", false)
	c.Assert(err, ErrorMatches, `other changes in progress \(conflicting change "unrelated"\), change "snapd downgrade" not allowed until they are done`)
}

func (s *snapmgrTestSuite) TestUpdateDowngradeBlockedByOtherChangesAlsoWhenEmpty(c *C) {
	err := s.testUpdateDowngradeBlockedByOtherChanges("2.57.1", "", false)
	c.Assert(err, ErrorMatches, `other changes in progress \(conflicting change "unrelated"\), change "snapd downgrade" not allowed until they are done`)
}

func (s *snapmgrTestSuite) TestUpdateDowngradeNotBlockedByOtherChanges(c *C) {
	err := s.testUpdateDowngradeBlockedByOtherChanges("2.57.1", "2.58", false)
	c.Assert(err, IsNil)
}

func (s *snapmgrTestSuite) TestRevertBlockedByOtherChanges(c *C) {
	// Swap values for revert case
	err := s.testUpdateDowngradeBlockedByOtherChanges("2.56", "2.57.1", true)
	c.Assert(err, ErrorMatches, `other changes in progress \(conflicting change "unrelated"\), change "snapd downgrade" not allowed until they are done`)
}

func (s *snapmgrTestSuite) TestRevertBlockedByOtherChangesAlsoWhenEmpty(c *C) {
	// Swap values for revert case
	err := s.testUpdateDowngradeBlockedByOtherChanges("2.58", "2.57.1", true)
	c.Assert(err, IsNil)
}

func (s *snapmgrTestSuite) testUpdateNotAllowedWhileDowngrading(c *C, old, new string, revert bool) error {
	si1 := snap.SideInfo{
		RealName: "snapd",
		SnapID:   "snapd-id",
		Channel:  "latest",
		Revision: snap.R(1),
	}
	si2 := snap.SideInfo{
		RealName: "snapd",
		SnapID:   "snapd-id",
		Channel:  "latest",
		Revision: snap.R(2),
	}
	si3 := snap.SideInfo{
		RealName: "snapd",
		SnapID:   "snapd-id",
		Channel:  "latest",
		Revision: snap.R(3),
	}

	si := snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Revision: snap.R(7),
		Channel:  "channel-for-7",
	}

	restore := snapstate.MockSnapReadInfo(func(name string, si *snap.SideInfo) (*snap.Info, error) {
		var version string
		switch name {
		case "snapd":
			if (revert && si.Revision.N == 1) || (!revert && si.Revision.N == 2) {
				version = old
			} else if (revert && si.Revision.N == 2) || si.Revision.N == 3 {
				version = new
			} else {
				return nil, fmt.Errorf("unexpected revision for test")
			}
		default:
			version = "1.0"
		}
		return &snap.Info{
			SuggestedName: name,
			Version:       version,
			Architectures: []string{"all"},
			SideInfo:      *si,
		}, nil
	})
	defer restore()

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

	snapstate.Set(s.state, "snapd", &snapstate.SnapState{
		Active:          true,
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si1, &si2, &si3}),
		TrackingChannel: "latest/stable",
		Current:         si2.Revision,
	})
	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:          true,
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si}),
		TrackingChannel: "other-chanel/stable",
		Current:         si.Revision,
	})

	var err error
	var ts *state.TaskSet
	if revert {
		ts, err = snapstate.Revert(s.state, "snapd", snapstate.Flags{}, "")
	} else {
		ts, err = snapstate.Update(s.state, "snapd", &snapstate.RevisionOptions{Revision: snap.R(3)}, s.user.ID, snapstate.Flags{})
	}
	c.Assert(err, IsNil)

	chg := s.state.NewChange("refresh-snap", "refresh snapd")
	chg.AddAll(ts)

	_, err = snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "channel-for-7/stable"}, s.user.ID, snapstate.Flags{})
	return err
}

func (s *snapmgrTestSuite) TestUpdateNotAllowedWhileDowngrading(c *C) {
	err := s.testUpdateNotAllowedWhileDowngrading(c, "2.57.1", "2.56", false)
	c.Assert(err, ErrorMatches, `snapd downgrade in progress, no other changes allowed until this is done`)
}

func (s *snapmgrTestSuite) TestUpdateNotAllowedWhileDowngradingAndWhenEmpty(c *C) {
	err := s.testUpdateNotAllowedWhileDowngrading(c, "2.57.1", "", false)
	c.Assert(err, ErrorMatches, `snapd downgrade in progress, no other changes allowed until this is done`)
}

func (s *snapmgrTestSuite) TestUpdateAllowedWhileUpgrading(c *C) {
	err := s.testUpdateNotAllowedWhileDowngrading(c, "2.57.1", "2.58", false)
	c.Assert(err, IsNil)
}

func (s *snapmgrTestSuite) TestUpdateNotAllowedWhileRevertDowngrading(c *C) {
	err := s.testUpdateNotAllowedWhileDowngrading(c, "2.56", "2.57.1", true)
	c.Assert(err, ErrorMatches, `snapd downgrade in progress, no other changes allowed until this is done`)
}

func (s *snapmgrTestSuite) TestUpdateAllowedWhileRevertUpgrading(c *C) {
	err := s.testUpdateNotAllowedWhileDowngrading(c, "2.58", "2.57.1", true)
	c.Assert(err, IsNil)
}

func (s *snapmgrTestSuite) TestUpdateSetsRestartBoundaries(c *C) {
	siGadget := snap.SideInfo{
		RealName: "brand-gadget",
		SnapID:   "brand-gadget-id",
		Revision: snap.R(7),
	}
	siSomeSnap := snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Revision: snap.R(7),
	}

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

	r := snapstatetest.MockDeviceModel(DefaultModel())
	defer r()
	snapstate.Set(s.state, "brand-gadget", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&siGadget}),
		Current:  siGadget.Revision,
	})
	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&siSomeSnap}),
		Current:  siSomeSnap.Revision,
	})

	ts1, err := snapstate.Update(s.state, "brand-gadget", nil, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)

	// only ensure that SetEssentialSnapsRestartBoundaries was actually called, we don't
	// test that all restart boundaries were set, one is enough
	linkSnap1 := ts1.MaybeEdge(snapstate.MaybeRebootEdge)
	c.Assert(linkSnap1, NotNil)

	var boundary restart.RestartBoundaryDirection
	c.Check(linkSnap1.Get("restart-boundary", &boundary), IsNil)

	// also ensure that it's not set for normal snaps
	ts2, err := snapstate.Update(s.state, "some-snap", nil, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)

	linkSnap2 := ts2.MaybeEdge(snapstate.MaybeRebootEdge)
	c.Assert(linkSnap2, NotNil)
	c.Check(linkSnap2.Get("restart-boundary", &boundary), ErrorMatches, `no state entry for key "restart-boundary"`)
}

func (s *snapmgrTestSuite) testUpdateManyRevOptsOrder(c *C, isThrottled map[string]bool, responseMode throttledRefreshResponseMode) {
	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)},
		}),
		Current:  snap.R(1),
		SnapType: "app",
	})
	snapstate.Set(s.state, "some-other-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-other-snap", SnapID: "some-other-snap-id", Revision: snap.R(1)},
		}),
		Current:  snap.R(1),
		SnapType: "app",
	})
	snapstate.Set(s.state, "snap-c", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "snap-c", SnapID: "snap-c-id", Revision: snap.R(1)},
		}),
		Current:  snap.R(1),
		SnapType: "app",
	})

	var requestSnapToAction map[string]*store.SnapAction
	sto := customStore{fakeStore: s.fakeStore}
	sto.customSnapAction = func(ctx context.Context, cs []*store.CurrentSnap, sa []*store.SnapAction, aq store.AssertionQuery, us *auth.UserState, ro *store.RefreshOptions) ([]store.SnapActionResult, []store.AssertionResult, error) {
		if len(sa) == 0 {
			return nil, nil, nil
		}

		var actionResult []store.SnapActionResult
		currentRevisionBySnapID := make(map[string]snap.Revision, len(cs))
		for _, cur := range cs {
			currentRevisionBySnapID[cur.SnapID] = cur.Revision
		}
		for _, action := range sa {
			requestSnapToAction[action.InstanceName] = action
			if isThrottled[action.SnapID] && ro.Scheduled && responseMode == throttledRefreshResponseOmit {
				continue
			}

			info, err := s.fakeStore.lookupRefresh(refreshCand{snapID: action.SnapID})
			c.Assert(err, IsNil)

			if isThrottled[action.SnapID] && ro.Scheduled && responseMode == throttledRefreshResponseEchoCurrent {
				info.Revision = currentRevisionBySnapID[action.SnapID]
			}
			actionResult = append(actionResult, store.SnapActionResult{Info: info})
		}

		return actionResult, nil, nil
	}
	snapstate.ReplaceStore(s.state, &sto)

	signer := assertstest.NewStoreStack("can0nical", nil)
	valSetForSnap := func(snapName string) *snapasserts.ValidationSets {
		headers := map[string]any{
			"authority-id": "foo",
			"account-id":   "foo",
			"name":         snapName,
			"series":       "16",
			"revision":     "5",
			"sequence":     "1",
			"timestamp":    "2030-11-06T09:16:26Z",
			"snaps": []any{
				map[string]any{
					"id":       snaptest.AssertedSnapID(snapName),
					"name":     snapName,
					"presence": "required",
				},
			},
		}

		vs, err := signer.Sign(asserts.ValidationSetType, headers, nil, "")
		c.Assert(err, IsNil)

		vsets := snapasserts.NewValidationSets()
		vsets.Add(vs.(*asserts.ValidationSet))

		return vsets
	}

	nameToRevOpts := map[string]*snapstate.RevisionOptions{
		"some-snap": {
			Revision:       snap.R(111),
			ValidationSets: valSetForSnap("some-snap"),
		},
		"some-other-snap": {
			Revision:       snap.R(222),
			ValidationSets: valSetForSnap("some-other-snap"),
		},
		"snap-c": {
			Revision:       snap.R(333),
			ValidationSets: valSetForSnap("snap-c"),
		},
	}
	getRevOpts := func(names []string) (revOpts []*snapstate.RevisionOptions) {
		for _, name := range names {
			revOpts = append(revOpts, nameToRevOpts[name])
		}
		return revOpts
	}

	testOrder := func(names []string) {
		requestSnapToAction = make(map[string]*store.SnapAction, 3)
		revOpts := getRevOpts(names)
		flags := snapstate.Flags{IsAutoRefresh: isThrottled != nil}
		_, _, err := snapstate.UpdateMany(context.Background(), s.state, names, revOpts, 0, &flags)
		c.Assert(err, IsNil)
		c.Check(requestSnapToAction, NotNil)
		for name, action := range requestSnapToAction {
			c.Check(action.Revision, Equals, nameToRevOpts[name].Revision, Commentf("snap %q sent revision is incorrect", name))
			c.Check(action.ValidationSets, DeepEquals, nameToRevOpts[name].ValidationSets.Keys(), Commentf("snap %q sent validation sets are incorrect", name))
		}
	}

	// let's check all permutations for good measure
	testOrder([]string{"some-snap", "some-other-snap", "snap-c"})
	testOrder([]string{"some-snap", "snap-c", "some-other-snap"})

	testOrder([]string{"some-other-snap", "some-snap", "snap-c"})
	testOrder([]string{"some-other-snap", "snap-c", "some-snap"})

	testOrder([]string{"snap-c", "some-snap", "some-other-snap"})
	testOrder([]string{"snap-c", "some-other-snap", "some-snap"})
}

func (s *snapmgrTestSuite) TestSnapdRefreshForRemodel(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	// Set snapd state in the system: currently rev 44 installed, but also
	// 22 is in the system (simply to avoid trying to reach the store for
	// that rev when we want to refresh to it).
	snapstate.Set(s.state, "snapd", &snapstate.SnapState{
		Active:          true,
		TrackingChannel: "latest/edge",
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "snapd", SnapID: "snapd-snap-id", Revision: snap.R(44)},
			{RealName: "snapd", SnapID: "snapd-snap-id", Revision: snap.R(22)}}),
		Current:  snap.R(44),
		SnapType: "app",
	})

	restore := snapstate.MockSnapReadInfo(func(name string, si *snap.SideInfo) (*snap.Info, error) {
		var version string
		switch name {
		case "snapd":
			switch si.Revision.N {
			case 22:
				version = "1.0"
			default:
				version = "2.0"
			}
		default:
			version = "1.0"
		}
		return &snap.Info{
			SuggestedName: name,
			Version:       version,
			Architectures: []string{"all"},
			SideInfo:      *si,
		}, nil
	})
	defer restore()

	// This is part of a remodeling change
	chg := s.state.NewChange("remodel", "...")
	chg.SetStatus(state.DoStatus)

	// Update to snapd rev 22, which is an older revision. This checks that
	// things are fine in that case and that there is no conflict detected
	// as we are doing this from the remodel change.
	opts := &snapstate.RevisionOptions{Channel: "stable", Revision: snap.R(22)}
	_, err := snapstate.UpdateWithDeviceContext(s.state, "snapd", opts, s.user.ID, snapstate.Flags{}, nil, nil, chg.ID())
	c.Check(err, IsNil)

	// But there is a conflict if this update is not part of the remodel change.
	_, err = snapstate.UpdateWithDeviceContext(s.state, "snapd", opts, s.user.ID, snapstate.Flags{}, nil, nil, "")
	c.Check(err, FitsTypeOf, &snapstate.ChangeConflictError{})
	c.Check(err, ErrorMatches, "remodeling in progress, no other changes allowed until this is done")
}

func (s *snapmgrTestSuite) TestUpdatePathWithDeviceContextLocalRevisionMismatch(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	si := &snap.SideInfo{RealName: "some-snap", Revision: snap.R(8)}
	_, err := snapstate.UpdatePathWithDeviceContext(s.state, si, "path", "some-snap", &snapstate.RevisionOptions{Revision: snap.R(7)}, s.user.ID, snapstate.Flags{}, nil, nil, "")
	c.Check(err, ErrorMatches, `cannot install local snap "some-snap": 7 != 8 \(revision mismatch\)`)
}

func (s *snapmgrTestSuite) TestUpdateManyRevOptsOrder(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	s.testUpdateManyRevOptsOrder(c, nil, throttledRefreshResponseEchoCurrent)
}

func (s *snapmgrTestSuite) TestRefreshCandidatesThrottledRevOptsRemap(c *C) {
	s.state.Lock()
	defer s.state.Unlock()
	s.testRefreshCandidatesThrottledRevOptsRemap(c, throttledRefreshResponseEchoCurrent)
}

func (s *snapmgrTestSuite) TestRefreshCandidatesThrottledRevOptsRemapOmitted(c *C) {
	s.state.Lock()
	defer s.state.Unlock()
	s.testRefreshCandidatesThrottledRevOptsRemap(c, throttledRefreshResponseOmit)
}

func (s *snapmgrTestSuite) testRefreshCandidatesThrottledRevOptsRemap(c *C, responseMode throttledRefreshResponseMode) {

	// simulate existing monitored refresh hint from older refresh
	cands := map[string]*snapstate.RefreshCandidate{
		"some-other-snap": {Monitored: true},
		"snap-c":          {Monitored: true},
	}
	s.state.Set("refresh-candidates", &cands)

	// simulate store throttling some snaps' during auto-refresh
	isThrottled := map[string]bool{
		"some-other-snap-id": true,
		"snap-c-id":          true,
	}

	s.testUpdateManyRevOptsOrder(c, isThrottled, responseMode)
}

func (s *snapmgrTestSuite) TestUpdateManyFilteredForSnapsNotInOldHints(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)},
		}),
		Current:  snap.R(1),
		SnapType: "app",
	})
	snapstate.Set(s.state, "some-other-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-other-snap", SnapID: "some-other-snap-id", Revision: snap.R(1)},
		}),
		Current:  snap.R(1),
		SnapType: "app",
	})

	// simulate existing refresh hint from older refresh with
	// some-other-snap being monitored
	cands := map[string]*snapstate.RefreshCandidate{
		"some-other-snap": {Monitored: true},
	}
	s.state.Set("refresh-candidates", &cands)

	storeSnapIDs := map[string]bool{}
	storeCalled := 0
	sto := customStore{fakeStore: s.fakeStore}
	sto.customSnapAction = func(ctx context.Context, cs []*store.CurrentSnap, sa []*store.SnapAction, aq store.AssertionQuery, us *auth.UserState, ro *store.RefreshOptions) ([]store.SnapActionResult, []store.AssertionResult, error) {
		storeCalled++

		var actionResult []store.SnapActionResult
		for _, action := range sa {
			storeSnapIDs[action.SnapID] = true
			info, err := s.fakeStore.lookupRefresh(refreshCand{snapID: action.SnapID})
			c.Assert(err, IsNil)
			actionResult = append(actionResult, store.SnapActionResult{Info: info})
		}

		return actionResult, nil, nil
	}
	snapstate.ReplaceStore(s.state, &sto)

	names := []string{"some-snap"}
	filterCalled := 0
	filter := func(info *snap.Info, s *snapstate.SnapState) bool {
		filterCalled++
		c.Check(info, NotNil)
		c.Check(info.InstanceName(), Equals, "some-snap")
		c.Check(s, NotNil)
		return true
	}
	flags := snapstate.Flags{IsAutoRefresh: true}

	updatedNames, tss, err := snapstate.UpdateManyFiltered(context.Background(), s.state, names, nil, 0, filter, &flags, "")
	c.Assert(err, IsNil)
	c.Assert(tss, NotNil)
	c.Check(updatedNames, DeepEquals, []string{"some-snap"})

	c.Check(storeCalled, Equals, 1)
	c.Check(filterCalled, Equals, 1)

	// check that only passed names are updated
	c.Check(storeSnapIDs, HasLen, 1)
	c.Check(storeSnapIDs["some-snap-id"], Equals, true)
	c.Check(storeSnapIDs["some-other-snap-id"], Equals, false)

	// check that refresh-candidates in the state were updated
	var newCands map[string]*snapstate.RefreshCandidate
	err = s.state.Get("refresh-candidates", &newCands)
	c.Assert(err, IsNil)

	c.Assert(newCands, HasLen, 2)
	c.Check(newCands["some-snap"], NotNil)
	// a monitored snap was preserved
	c.Assert(newCands["some-other-snap"], NotNil)
	c.Check(newCands["some-other-snap"].Monitored, Equals, true)
}

func (s *snapmgrTestSuite) TestUpdateManyFilteredNotAutoRefreshNoRetry(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)},
		}),
		Current:  snap.R(1),
		SnapType: "app",
	})
	snapstate.Set(s.state, "some-other-snap", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-other-snap", SnapID: "some-other-snap-id", Revision: snap.R(1)},
		}),
		Current:  snap.R(1),
		SnapType: "app",
	})

	// simulate existing refresh hint from older refresh with
	// some-other-snap being monitored
	cands := map[string]*snapstate.RefreshCandidate{
		"some-other-snap": {Monitored: true},
	}
	s.state.Set("refresh-candidates", &cands)

	storeSnapIDs := map[string]bool{}
	storeCalled := 0
	sto := customStore{fakeStore: s.fakeStore}
	sto.customSnapAction = func(ctx context.Context, cs []*store.CurrentSnap, sa []*store.SnapAction, aq store.AssertionQuery, us *auth.UserState, ro *store.RefreshOptions) ([]store.SnapActionResult, []store.AssertionResult, error) {
		storeCalled++

		var actionResult []store.SnapActionResult
		currentRevisionBySnapID := make(map[string]snap.Revision, len(cs))
		for _, cur := range cs {
			currentRevisionBySnapID[cur.SnapID] = cur.Revision
		}
		for _, action := range sa {
			storeSnapIDs[action.SnapID] = true

			info, err := s.fakeStore.lookupRefresh(refreshCand{snapID: action.SnapID})
			c.Assert(err, IsNil)

			// throttle some-other-snap by returning the revision from the request
			// context.
			if action.SnapID == "some-other-snap-id" {
				info.Revision = currentRevisionBySnapID[action.SnapID]
			}
			actionResult = append(actionResult, store.SnapActionResult{Info: info})
		}

		return actionResult, nil, nil
	}
	snapstate.ReplaceStore(s.state, &sto)

	names := []string{"some-snap", "some-other-snap"}
	flags := snapstate.Flags{IsAutoRefresh: false}

	updatedNames, tss, err := snapstate.UpdateManyFiltered(context.Background(), s.state, names, nil, 0, nil, &flags, "")
	c.Assert(err, IsNil)
	c.Assert(tss, NotNil)
	c.Check(updatedNames, DeepEquals, []string{"some-snap"})

	// no retry should be attempted because this is not an auto-refresh
	c.Check(storeCalled, Equals, 1)

	c.Check(storeSnapIDs, HasLen, 2)
	c.Check(storeSnapIDs["some-snap-id"], Equals, true)
	c.Check(storeSnapIDs["some-other-snap-id"], Equals, true)
}

func (s *snapmgrTestSuite) TestUpdateSnapdAndSnapPullingNewBase(c *C) {
	// classic = true, avoid messing with snapd services
	restore := release.MockOnClassic(true)
	defer restore()

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

	fakeAutoConnect := func(task *state.Task, _ *tomb.Tomb) error {
		st := task.State()
		st.Lock()
		defer st.Unlock()
		kind := task.Kind()
		status := task.Status()
		snapsup, err := snapstate.TaskSnapSetup(task)
		if err != nil {
			return err
		}
		if status == state.DoingStatus {
			if snapsup.Type == snap.TypeSnapd {
				si := snapsup.SideInfo
				// snapd is installed by now
				snaptest.MockSnapCurrent(c, `
name: snapd
version: snapdVer
type: snapd
`, snapsup.SideInfo)
				snapstate.Set(st, "snapd", &snapstate.SnapState{
					SnapType:        "snapd",
					Active:          true,
					Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
					Current:         si.Revision,
					Flags:           snapstate.Flags{Required: true},
					TrackingChannel: si.Channel,
				})
			}
			// fake restart
			restart.MockPending(st, restart.RestartUnset)
			if err := snapstate.FinishRestart(task, snapsup,
				snapstate.FinishRestartOptions{FinishRestartDefault: true}); err != nil {
				panic(err)
			}
		}
		return s.fakeBackend.ForeignTask(kind, status, snapsup, nil)
	}

	s.o.TaskRunner().AddHandler("auto-connect", fakeAutoConnect, fakeAutoConnect)

	snapstate.Set(s.state, "core22", nil) // NOTE: core22 is not installed.
	snapstate.Set(s.state, "snap-core18-to-core22", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{
				RealName: "snap-core18-to-core22",
				SnapID:   "snap-core18-to-core22-id",
				Revision: snap.R(1),
			},
		}),
		Current:  snap.R(1),
		SnapType: "app",
		Base:     "core18",
	})
	snapstate.Set(s.state, "snapd", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{
				RealName: "snapd",
				SnapID:   "snapd-without-version-id",
				Revision: snap.R(1),
			},
		}),
		Current:  snap.R(1),
		SnapType: "snapd",
	})

	updated, taskSets, err := snapstate.UpdateMany(context.Background(), s.state,
		[]string{"snapd", "snap-core18-to-core22"},
		[]*snapstate.RevisionOptions{{
			Channel: "latest/stable",
		}, {
			Channel: "some-channel",
		}},
		s.user.ID, &snapstate.Flags{
			IgnoreRunning: true,
			Transaction:   client.TransactionPerSnap, // XXX: it is unclear if this represents the real-world behavior.
		})
	c.Assert(err, IsNil)

	chg := s.state.NewChange("refresh-snap", "refresh snapd and some other snap pulling in a base")

	for _, taskSet := range taskSets {
		chg.AddAll(taskSet)
	}

	// XXX: This mimics newChange from daemon/api.go
	if updated != nil {
		chg.Set("snap-names", updated)
	}

	s.state.EnsureBefore(0)

	// up until link-snap requesting daemon restart after snapd
	s.settle(c)

	kind := restart.Pending(s.state)
	c.Check(kind, Equals, restart.RestartDaemon)
	restart.MockPending(s.state, restart.RestartUnset)

	// up until link-snap requesting daemon restart after snapd
	s.settle(c)

	didDownloadCore22 := false

	for _, fakeOp := range s.fakeBackend.ops {
		if fakeOp.op == "storesvc-download" && fakeOp.name == "core22" {
			didDownloadCore22 = true
		}
	}

	c.Assert(didDownloadCore22, Equals, true, Commentf("core22 was *not* downloaded"))

	// check that the snaps's state reflects the new base
	var snapst snapstate.SnapState
	err = snapstate.Get(s.state, "snap-core18-to-core22", &snapst)
	c.Assert(err, IsNil)
	c.Assert(snapst.Base, Equals, "core22")

	chg = s.state.Change(chg.ID())
	c.Check(chg.Status(), Equals, state.DoneStatus)
	c.Check(chg.Err(), IsNil)
}

func (s *snapmgrTestSuite) TestUndoUpdateSnapdAndSnapPullingNewBase(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	// core22 is the base of the next revision of snap-core18-to-core22
	snapstate.Set(s.state, "core22", nil)
	snapstate.Set(s.state, "snap-core18-to-core22", &snapstate.SnapState{
		Active: true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{
				RealName: "snap-core18-to-core22",
				SnapID:   "snap-core18-to-core22-id",
				Revision: snap.R(1),
			},
		}),
		Current:  snap.R(1),
		SnapType: "app",
		Base:     "core18",
	})

	_, taskSets, err := snapstate.UpdateMany(context.Background(), s.state,
		[]string{"snap-core18-to-core22"},
		[]*snapstate.RevisionOptions{{
			Channel: "latest/stable",
		}, {
			Channel: "some-channel",
		}},
		s.user.ID, nil)
	c.Assert(err, IsNil)

	chg := s.state.NewChange("refresh-snap", "failing to refresh snap pulling in a base")
	for _, taskSet := range taskSets {
		chg.AddAll(taskSet)
	}

	linkTask := findLastTask(chg, "link-snap")
	// fail the change after the snap's new state is saved
	s.o.TaskRunner().AddHandler("fail", func(*state.Task, *tomb.Tomb) error {
		return errors.New("expected")
	}, nil)

	failingTask := s.state.NewTask("fail", "expected failure")
	chg.AddTask(failingTask)
	failingTask.WaitFor(linkTask)
	for _, lane := range linkTask.Lanes() {
		failingTask.JoinLane(lane)
	}

	s.settle(c)

	var downloadedCore22 bool
	for _, fakeOp := range s.fakeBackend.ops {
		if fakeOp.op == "storesvc-download" && fakeOp.name == "core22" {
			downloadedCore22 = true
		}
	}
	c.Assert(downloadedCore22, Equals, true)
	c.Assert(chg.Status(), Equals, state.ErrorStatus)

	// check that the snaps's state was correctly reverted
	var snapst snapstate.SnapState
	err = snapstate.Get(s.state, "snap-core18-to-core22", &snapst)
	c.Assert(err, IsNil)
	c.Assert(snapst.Base, Equals, "core18")
}

// prepare a refresh/install of essential and non-essential snaps, optionally
// with an app depending on the model base, to test that the update doesn't make
// apps wait for the reboot required by the essential snaps.
func (s *snapmgrTestSuite) setupSplitRefreshAppDependsOnModelBase(c *C, core18BasedApp bool) (names []string, infos []*snap.SideInfo) {
	restore := snapstatetest.MockDeviceModel(ModelWithBase("core18"))
	s.AddCleanup(restore)

	snaps := []string{"snapd", "kernel", "core18", "gadget", "some-base", "some-base-snap"}
	if core18BasedApp {
		// add an app that depends on the model base so test a cross set dependency
		snaps = append(snaps, "some-snap-with-core18-base")
		// we expect an app to have to wait for a base, shorten the retry timeout
		restore = snapstate.MockPrerequisitesRetryTimeout(200 * time.Millisecond)
		s.AddCleanup(restore)
	} else {
		snaps = append(snaps, "some-snap")
	}

	types := map[string]string{
		"snapd":                      "snapd",
		"core18":                     "base",
		"gadget":                     "gadget",
		"kernel":                     "kernel",
		"some-base":                  "base",
		"some-base-snap":             "app",
		"some-snap-with-core18-base": "app",
		"some-snap":                  "app",
	}
	snapIds := map[string]string{
		"snapd":                      "snapd-snap-id",
		"core18":                     "core18-snap-id",
		"gadget":                     "gadget-core18-id",
		"kernel":                     "kernel-id",
		"some-base":                  "some-base-id",
		"some-snap-with-core18-base": "some-snap-with-core18-base",
		"some-base-snap":             "some-base-snap-id",
		"some-snap":                  "some-snap-id",
	}
	bases := map[string]string{
		// create a dependency between the two task sets
		"some-snap-with-core18-base": "core18",
		"some-base-snap":             "some-base",
	}

	var paths []string
	for _, sn := range snaps {
		yaml := fmt.Sprintf("name: %s\nversion: 1.0\nepoch: 1\ntype: %s", sn, types[sn])
		if base, ok := bases[sn]; ok {
			yaml += fmt.Sprintf("\nbase: %s", base)
		}

		oldSi := &snap.SideInfo{RealName: sn, SnapID: snapIds[sn], Revision: snap.R(1)}
		newSi := &snap.SideInfo{RealName: sn, SnapID: snapIds[sn], Revision: snap.R(11)}

		path, _ := snaptest.MakeTestSnapInfoWithFiles(c, yaml, nil, newSi)
		paths = append(paths, path)
		infos = append(infos, newSi)

		snaptest.MockSnap(c, yaml, oldSi)
		snapstate.Set(s.state, sn, &snapstate.SnapState{
			Active:          true,
			Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{oldSi}),
			Current:         oldSi.Revision,
			TrackingChannel: "latest/stable",
			SnapType:        types[sn],
		})
	}

	s.fakeBackend.linkSnapRebootFor = map[string]bool{
		"core18": true,
		"kernel": true,
		"gadget": true,
	}
	s.fakeBackend.linkSnapMaybeReboot = true

	s.o.TaskRunner().AddHandler("update-gadget-assets",
		func(task *state.Task, tomb *tomb.Tomb) error {
			task.State().Lock()
			defer task.State().Unlock()
			chg := task.Change()
			chg.Set("gadget-restart-required", true)
			return nil
		},
		func(task *state.Task, tomb *tomb.Tomb) error { return nil })

	s.o.TaskRunner().AddHandler("update-gadget-cmdline",
		func(task *state.Task, tomb *tomb.Tomb) error { return nil },
		func(task *state.Task, tomb *tomb.Tomb) error { return nil })

	return paths, infos
}

func (s *snapmgrTestSuite) TestUpdateManySplitEssentialWithSharedBase(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	restore := release.MockOnClassic(true)
	s.AddCleanup(restore)

	sharedBase := true
	_, infos := s.setupSplitRefreshAppDependsOnModelBase(c, sharedBase)

	var snaps []string
	for _, info := range infos {
		snaps = append(snaps, info.RealName)
	}

	chg := s.state.NewChange("refresh", fmt.Sprintf("refresh %v", snaps))
	affected, tss, err := snapstate.UpdateMany(context.Background(), s.state, snaps, nil, s.user.ID, nil)
	c.Assert(err, IsNil)
	c.Assert(affected, testutil.DeepUnsortedMatches, snaps)

	for _, ts := range tss {
		chg.AddAll(ts)
	}

	c.Check(chg.CheckTaskDependencies(), IsNil)

	// up until daemon restart caused by snapd installation
	s.settle(c)
	kind := restart.Pending(s.state)
	c.Check(kind, Equals, restart.RestartDaemon)
	restart.MockPending(s.state, restart.RestartUnset)
	// run through the end
	s.settle(c)

	checkRerefresh := true
	s.checkSplitRefreshWithSharedBase(c, chg, checkRerefresh)
}

func (s *snapmgrTestSuite) TestOldStyleAutoRefreshSplitEssentialWithSharedBase(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	restore := release.MockOnClassic(true)
	defer restore()
	restore = snapstatetest.MockDeviceModel(ModelWithBase("core18"))
	defer restore()
	restore = snapstate.MockPrerequisitesRetryTimeout(200 * time.Millisecond)
	defer restore()

	snaps := []string{"snapd", "kernel", "core18", "gadget", "some-snap-with-core18-base"}

	types := map[string]string{
		"snapd":                      "snapd",
		"core18":                     "base",
		"gadget":                     "gadget",
		"kernel":                     "kernel",
		"some-snap-with-core18-base": "app",
	}
	snapIds := map[string]string{
		"snapd":                      "snapd-snap-id",
		"core18":                     "core18-snap-id",
		"gadget":                     "gadget-core18-id",
		"kernel":                     "kernel-id",
		"some-snap-with-core18-base": "some-snap-with-core18-base",
	}
	bases := map[string]string{
		// create a dependency between the two task sets
		"some-snap-with-core18-base": "core18",
	}

	for _, sn := range snaps {
		yaml := fmt.Sprintf("name: %s\nversion: 1.0\nepoch: 1\ntype: %s", sn, types[sn])
		if base, ok := bases[sn]; ok {
			yaml += fmt.Sprintf("\nbase: %s", base)
		}

		oldSi := &snap.SideInfo{RealName: sn, SnapID: snapIds[sn], Revision: snap.R(1)}
		newSi := &snap.SideInfo{RealName: sn, SnapID: snapIds[sn], Revision: snap.R(11)}

		snaptest.MakeTestSnapInfoWithFiles(c, yaml, nil, newSi)

		snaptest.MockSnap(c, yaml, oldSi)
		snapstate.Set(s.state, sn, &snapstate.SnapState{
			Active:          true,
			Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{oldSi}),
			Current:         oldSi.Revision,
			TrackingChannel: "latest/stable",
			SnapType:        types[sn],
		})
	}

	s.fakeBackend.linkSnapRebootFor = map[string]bool{
		"core18": true,
		"kernel": true,
		"gadget": true,
	}
	s.fakeBackend.linkSnapMaybeReboot = true

	s.o.TaskRunner().AddHandler("update-gadget-assets",
		func(task *state.Task, tomb *tomb.Tomb) error {
			task.State().Lock()
			defer task.State().Unlock()
			chg := task.Change()
			chg.Set("gadget-restart-required", true)
			return nil
		},
		func(task *state.Task, tomb *tomb.Tomb) error { return nil })

	s.o.TaskRunner().AddHandler("update-gadget-cmdline",
		func(task *state.Task, tomb *tomb.Tomb) error { return nil },
		func(task *state.Task, tomb *tomb.Tomb) error { return nil })

	chg := s.state.NewChange("auto-refresh", fmt.Sprintf("auto-refresh %v", snaps))
	affected, tss, err := snapstate.AutoRefresh(context.Background(), s.state)
	c.Assert(err, IsNil)
	c.Assert(affected, testutil.DeepUnsortedMatches, snaps)

	for _, ts := range tss.Refresh {
		chg.AddAll(ts)
	}

	c.Check(chg.CheckTaskDependencies(), IsNil)

	// up until daemon restart caused by snapd installation
	s.settle(c)
	kind := restart.Pending(s.state)
	c.Check(kind, Equals, restart.RestartDaemon)
	restart.MockPending(s.state, restart.RestartUnset)
	// keep running until the change hits a wait status
	s.settle(c)

	// some-snap-with-core18-base depends on the base but the prereq code only waits
	// for the base link-snap so it can complete before the reboot
	for _, snap := range []string{"snapd", "some-snap-with-core18-base"} {
		t := findTaskForSnap(c, chg, "auto-connect", snap)
		c.Assert(t.Status(), Equals, state.DoneStatus, Commentf("expected task %q for %q to be \"Done\": %s", t.Kind(), snap, t.Status()))
	}

	for _, snap := range []string{"kernel", "gadget", "core18"} {
		t := findTaskForSnap(c, chg, "auto-connect", snap)
		c.Assert(t.Status(), Equals, state.DoStatus, Commentf("expected task %q for %q to be \"Do\": %s", t.Kind(), snap, t.Status()))
	}

	// check that the rerefresh task is done because the essential tasks are
	// ignored
	rerefreshTask := findLastTask(chg, "check-rerefresh")
	c.Assert(rerefreshTask, NotNil, Commentf("cannot find check-rerefresh task"))
	c.Assert(rerefreshTask.Status(), Equals, state.DoneStatus)

	t := findTaskForSnap(c, chg, "link-snap", "kernel")
	c.Assert(t.Status(), Equals, state.WaitStatus, Commentf("expected kernel's link-snap to be waiting for restart"))
	s.mockRestartAndSettle(c, chg)

	for _, snap := range []string{"kernel", "gadget", "core18", "some-snap-with-core18-base"} {
		t := findTaskForSnap(c, chg, "auto-connect", snap)
		c.Assert(t.Status(), Equals, state.DoneStatus, Commentf("expected task %q for %q to be in \"Done\": %s", t.Kind(), snap, t.Status()))
	}

	c.Check(chg.IsReady(), Equals, true)
	c.Check(chg.Status(), Equals, state.DoneStatus)
}

func (s *snapmgrTestSuite) checkSplitRefreshWithSharedBase(c *C, chg *state.Change, checkRerefresh bool) {
	// some-snap-with-core18-base depends on the base but the prereq code only waits
	// for the base link-snap so it can complete before the reboot
	for _, snap := range []string{"snapd", "some-base", "some-base-snap", "some-snap-with-core18-base"} {
		t := findTaskForSnap(c, chg, "auto-connect", snap)
		c.Assert(t.Status(), Equals, state.DoneStatus, Commentf("expected task %q for %q to be \"Done\": %s", t.Kind(), snap, t.Status()))
	}

	for _, snap := range []string{"kernel", "gadget", "core18"} {
		t := findTaskForSnap(c, chg, "auto-connect", snap)
		c.Assert(t.Status(), Equals, state.DoStatus, Commentf("expected task %q for %q to be \"Do\": %s", t.Kind(), snap, t.Status()))
	}

	if checkRerefresh {
		// check that the rerefresh task is done because the essential tasks are
		// ignored
		rerefreshTask := findLastTask(chg, "check-rerefresh")
		c.Assert(rerefreshTask, NotNil, Commentf("cannot find check-rerefresh task"))
		c.Assert(rerefreshTask.Status(), Equals, state.DoneStatus)
	}

	t := findTaskForSnap(c, chg, "link-snap", "kernel")
	c.Assert(t.Status(), Equals, state.WaitStatus, Commentf("expected kernel's link-snap to be waiting for restart"))
	s.mockRestartAndSettle(c, chg)

	for _, snap := range []string{"kernel", "gadget", "core18", "some-snap-with-core18-base"} {
		t := findTaskForSnap(c, chg, "auto-connect", snap)
		c.Assert(t.Status(), Equals, state.DoneStatus, Commentf("expected task %q for %q to be in \"Done\": %s", t.Kind(), snap, t.Status()))
	}

	c.Check(chg.IsReady(), Equals, true)
	c.Check(chg.Status(), Equals, state.DoneStatus)
}

func (s *snapmgrTestSuite) TestUpdateManySplitEssentialWithoutSharedBase(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	restore := release.MockOnClassic(true)
	s.AddCleanup(restore)

	sharedBase := false
	_, infos := s.setupSplitRefreshAppDependsOnModelBase(c, sharedBase)

	var snaps []string
	for _, info := range infos {
		snaps = append(snaps, info.RealName)
	}

	chg := s.state.NewChange("refresh", fmt.Sprintf("refresh %v", snaps))
	affected, tss, err := snapstate.UpdateMany(context.Background(), s.state, snaps, nil, s.user.ID, nil)
	c.Assert(err, IsNil)
	c.Assert(affected, testutil.DeepUnsortedMatches, snaps)

	for _, ts := range tss {
		chg.AddAll(ts)
	}

	c.Check(chg.CheckTaskDependencies(), IsNil)

	// up until daemon restart caused by snapd installation
	s.settle(c)
	kind := restart.Pending(s.state)
	c.Check(kind, Equals, restart.RestartDaemon)
	restart.MockPending(s.state, restart.RestartUnset)
	// run through the end
	s.settle(c)

	for _, snap := range []string{"snapd", "some-snap", "some-base", "some-base-snap"} {
		t := findTaskForSnap(c, chg, "auto-connect", snap)
		c.Assert(t.Status(), Equals, state.DoneStatus, Commentf("expected task %q for snap %q to be done: %s", t.Kind(), snap, t.Status()))
	}

	for _, snap := range []string{"kernel", "gadget", "core18"} {
		t := findTaskForSnap(c, chg, "auto-connect", snap)
		c.Assert(t.Status(), Equals, state.DoStatus, Commentf("expected task %q for snap %q to be do: %s", t.Kind(), snap, t.Status()))
	}

	// check that the check-rerefresh task completed because it only considers
	// non-essential snap refreshes and those can finish before the reboot
	var rerefreshTask *state.Task
	for _, ts := range tss {
		if len(ts.Tasks()) == 1 && ts.Tasks()[0].Kind() == "check-rerefresh" {
			rerefreshTask = ts.Tasks()[0]
			break
		}
	}
	c.Assert(rerefreshTask, NotNil, Commentf("cannot find check-rerefresh task"))
	c.Assert(rerefreshTask.Status(), Equals, state.DoneStatus)

	t := findTaskForSnap(c, chg, "link-snap", "kernel")
	c.Assert(t.Status(), Equals, state.WaitStatus, Commentf("expected kernel's link-snap to be waiting for restart"))

	// waiting for reboot
	for _, snap := range []string{"kernel", "gadget", "core18"} {
		t := findTaskForSnap(c, chg, "auto-connect", snap)
		c.Assert(t.Status(), Equals, state.DoStatus, Commentf("expected task %q for snap %q to be do: %s", t.Kind(), snap, t.Status()))
	}
	s.mockRestartAndSettle(c, chg)

	// completed after restart
	for _, snap := range []string{"kernel", "gadget", "core18"} {
		t := findTaskForSnap(c, chg, "auto-connect", snap)
		c.Assert(t.Status(), Equals, state.DoneStatus, Commentf("expected task %q for snap %q to be done: %s", t.Kind(), snap, t.Status()))
	}

	c.Check(chg.IsReady(), Equals, true)
	c.Check(chg.Status(), Equals, state.DoneStatus)
}

func (s *snapmgrTestSuite) TestSplitRefreshUsesSameTransaction(c *C) {
	const createLane = false
	s.testSplitRefreshUsesSameTransaction(c, createLane)
}

func (s *snapmgrTestSuite) TestSplitRefreshUsesProvidedTransactionLane(c *C) {
	const createLane = true
	s.testSplitRefreshUsesSameTransaction(c, createLane)
}

func (s *snapmgrTestSuite) testSplitRefreshUsesSameTransaction(c *C, createLane bool) {
	s.state.Lock()
	defer s.state.Unlock()

	providedLane := 0
	if createLane {
		providedLane = s.state.NewLane()
	}

	restore := snapstatetest.MockDeviceModel(ModelWithBase("core18"))
	defer restore()

	restore = release.MockOnClassic(true)
	defer restore()

	s.fakeBackend.linkSnapRebootFor = map[string]bool{"kernel": true}
	s.fakeBackend.linkSnapMaybeReboot = true

	s.o.TaskRunner().AddHandler("fail", func(*state.Task, *tomb.Tomb) error {
		return fmt.Errorf("expected error")
	}, nil)

	snaps := []string{"kernel", "some-snap"}
	types := map[string]string{
		"kernel":    "kernel",
		"some-snap": "app",
	}
	snapIds := map[string]string{
		"kernel":    "kernel-id",
		"some-snap": "some-snap-id",
	}

	for _, sn := range snaps {
		si := &snap.SideInfo{RealName: sn, Revision: snap.R(1), SnapID: snapIds[sn]}
		snapYaml := fmt.Sprintf("name: %s\nversion: 1.2.3\ntype: %s", sn, types[sn])
		snaptest.MockSnap(c, snapYaml, si)
		snapstate.Set(s.state, sn, &snapstate.SnapState{
			Active:          true,
			Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
			Current:         si.Revision,
			TrackingChannel: "latest/stable",
			SnapType:        types[sn],
		})
	}

	affected, tss, err := snapstate.UpdateMany(context.Background(), s.state, snaps, nil, s.user.ID, &snapstate.Flags{
		NoReRefresh: true,
		Transaction: client.TransactionAllSnaps,
		Lane:        providedLane,
	})
	c.Assert(err, IsNil)
	c.Assert(affected, testutil.DeepUnsortedMatches, snaps)

	lanes := tss[0].Tasks()[0].Lanes()
	c.Assert(lanes, HasLen, 1)
	lane := lanes[0]
	c.Assert(lane, Not(Equals), 0)
	if createLane {
		c.Assert(lane, Equals, providedLane)
	}

	// fail the kernel refresh at the end
	ts, err := snapstate.MaybeFindTasksetForSnap(tss, "kernel")
	c.Assert(err, IsNil)
	lastTask := ts.MaybeEdge(snapstate.EndEdge)
	failTask := s.state.NewTask("fail", "")
	failTask.JoinLane(lane)
	failTask.WaitFor(lastTask)
	ts.AddTask(failTask)

	chg := s.state.NewChange("refresh", fmt.Sprintf("refresh %v", snaps))
	for _, ts := range tss {
		chg.AddAll(ts)
	}

	for _, ts := range tss {
		for _, t := range ts.Tasks() {
			c.Assert(t.Lanes(), DeepEquals, []int{lane})
		}
	}

	s.settle(c)

	t := findTaskForSnap(c, chg, "auto-connect", "some-snap")
	c.Assert(t.Status(), Equals, state.DoneStatus)

	t = findTaskForSnap(c, chg, "link-snap", "kernel")
	c.Assert(t.Status(), Equals, state.WaitStatus)

	s.mockRestartAndSettle(c, chg)

	for _, name := range []string{"kernel", "some-snap"} {
		t := findTaskForSnap(c, chg, "auto-connect", name)
		c.Assert(t.Status(), Equals, state.UndoneStatus)
	}

	c.Check(chg.Status(), Equals, state.ErrorStatus)
}

func (s *snapmgrTestSuite) TestSplitEssentialSnapUpdates(c *C) {
	updatesToNames := func(updates []snapstate.SnapUpdate) []string {
		names := make([]string, 0, len(updates))
		for _, up := range updates {
			names = append(names, up.Setup.InstanceName())
		}
		return names
	}

	types := map[string]string{
		"snapd":          "snapd",
		"core18":         "base",
		"gadget":         "gadget",
		"kernel":         "kernel",
		"some-base":      "base",
		"some-base-snap": "app",
		"some-snap":      "app",
	}

	type testcase struct {
		modelBase         string
		snaps             []string
		bases             map[string]string
		essentialSnaps    []string
		nonEssentialSnaps []string
	}

	tcs := []testcase{
		{
			modelBase: "core18",
			snaps:     []string{"snapd", "kernel", "core18", "gadget", "some-snap", "some-base", "some-base-snap"},
			bases:     map[string]string{"some-base-snap": "some-base", "some-snap": "core18"},
			// core18 is in the essential group because it's the model base
			essentialSnaps:    []string{"snapd", "kernel", "core18", "gadget"},
			nonEssentialSnaps: []string{"some-snap", "some-base", "some-base-snap"},
		},
		{
			modelBase:      "core22",
			snaps:          []string{"snapd", "kernel", "core18", "gadget", "some-snap", "some-base", "some-base-snap"},
			bases:          map[string]string{"some-base-snap": "some-base", "some-snap": "core18"},
			essentialSnaps: []string{"snapd", "kernel", "gadget"},
			// core18 is in the non-essential group w/ its dependency bc it's not the model base
			nonEssentialSnaps: []string{"some-snap", "some-base", "some-base-snap", "core18"},
		},
		{
			modelBase: "core18",
			snaps:     []string{"snapd", "some-snap", "some-base", "some-base-snap"},
			bases:     map[string]string{"some-base-snap": "some-base", "some-snap": "core18"},
			// snapd is in the non-essential taskset because there are no other essential snaps
			nonEssentialSnaps: []string{"some-snap", "some-base", "some-base-snap", "snapd"},
		},
		{
			modelBase: "core18",
			snaps:     []string{"snapd", "some-snap", "core18"},
			bases:     map[string]string{"some-snap": "core18"},
			// no kernel/gadget so snapd and core18 can be refreshed with the app
			nonEssentialSnaps: []string{"some-snap", "core18", "snapd"},
		},
	}

	for _, tc := range tcs {
		updates := make([]snapstate.SnapUpdate, 0, len(tc.snaps))
		for _, sn := range tc.snaps {
			updates = append(updates, snapstate.SnapUpdate{
				Setup: snapstate.SnapSetup{
					SideInfo: &snap.SideInfo{RealName: sn, Revision: snap.R(1), SnapID: sn + "-id"},
					Type:     snap.Type(types[sn]),
					Base:     tc.bases[sn],
				},
			})
		}

		ctx := &snapstatetest.TrivialDeviceContext{DeviceModel: ModelWithBase(tc.modelBase)}
		essential, nonEssential := snapstate.SplitEssentialUpdates(ctx, updates)
		c.Assert(updatesToNames(essential), testutil.DeepUnsortedMatches, tc.essentialSnaps)
		c.Assert(updatesToNames(nonEssential), testutil.DeepUnsortedMatches, tc.nonEssentialSnaps)
	}
}

type hybridContentStore struct {
	*fakeStore
	state *state.State
}

func (s hybridContentStore) SnapAction(ctx context.Context, currentSnaps []*store.CurrentSnap, actions []*store.SnapAction, assertQuery store.AssertionQuery, user *auth.UserState, opts *store.RefreshOptions) ([]store.SnapActionResult, []store.AssertionResult, error) {
	sars, _, err := s.fakeStore.SnapAction(ctx, currentSnaps, actions, assertQuery, user, opts)
	if err != nil {
		return nil, nil, err
	}

	var res []store.SnapActionResult
	for _, s := range sars {
		info := s.Info
		switch info.InstanceName() {
		case "snap-content-plug":
			info.Plugs = map[string]*snap.PlugInfo{
				"some-plug": {
					Snap:      info,
					Name:      "shared-content",
					Interface: "content",
					Attrs: map[string]any{
						"default-provider": "snap-content-slot",
						"content":          "shared-content",
					},
				},
			}
		case "snap-content-slot":
			info.Slots = map[string]*snap.SlotInfo{
				"some-slot": {
					Snap:      info,
					Name:      "shared-content",
					Interface: "content",
					Attrs: map[string]any{
						"content": "shared-content",
					},
				},
			}
			// default provider depends on the model base
			info.Base = "core18"
		}
		res = append(res, store.SnapActionResult{Info: info})
	}
	return res, nil, err
}

func (s *snapmgrTestSuite) TestSplitRefreshWithDefaultProviderDependingOnModelBase(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	snapstate.ReplaceStore(s.state, hybridContentStore{fakeStore: s.fakeStore, state: s.state})
	repo := interfaces.NewRepository()
	ifacerepo.Replace(s.state, repo)

	restore := release.MockOnClassic(true)
	defer restore()
	restore = snapstatetest.MockDeviceModel(ModelWithBase("core18"))
	defer restore()
	restore = snapstate.MockPrerequisitesRetryTimeout(200 * time.Millisecond)
	defer restore()

	snaps := []string{"snapd", "kernel", "core18", "gadget", "snap-content-plug"}
	types := map[string]string{
		"snapd":             "snapd",
		"core18":            "base",
		"gadget":            "gadget",
		"kernel":            "kernel",
		"snap-content-plug": "app",
	}
	snapIds := map[string]string{
		"snapd":             "snapd-snap-id",
		"core18":            "core18-snap-id",
		"gadget":            "gadget-core18-id",
		"kernel":            "kernel-id",
		"snap-content-plug": "snap-content-plug-id",
	}

	for _, sn := range snaps {
		yaml := fmt.Sprintf("name: %s\nversion: 1.0\nepoch: 1\ntype: %s", sn, types[sn])

		oldSi := &snap.SideInfo{RealName: sn, SnapID: snapIds[sn], Revision: snap.R(1)}
		newSi := &snap.SideInfo{RealName: sn, SnapID: snapIds[sn], Revision: snap.R(11)}

		snaptest.MakeTestSnapInfoWithFiles(c, yaml, nil, newSi)

		snaptest.MockSnap(c, yaml, oldSi)
		snapstate.Set(s.state, sn, &snapstate.SnapState{
			Active:          true,
			Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{oldSi}),
			Current:         oldSi.Revision,
			TrackingChannel: "latest/stable",
			SnapType:        types[sn],
		})
	}

	s.fakeBackend.linkSnapRebootFor = map[string]bool{
		"core18": true,
		"kernel": true,
		"gadget": true,
	}
	s.fakeBackend.linkSnapMaybeReboot = true

	s.o.TaskRunner().AddHandler("update-gadget-assets",
		func(task *state.Task, tomb *tomb.Tomb) error {
			task.State().Lock()
			defer task.State().Unlock()
			chg := task.Change()
			chg.Set("gadget-restart-required", true)
			return nil
		},
		func(task *state.Task, tomb *tomb.Tomb) error { return nil })

	s.o.TaskRunner().AddHandler("update-gadget-cmdline",
		func(task *state.Task, tomb *tomb.Tomb) error { return nil },
		func(task *state.Task, tomb *tomb.Tomb) error { return nil })

	chg := s.state.NewChange("refresh", fmt.Sprintf("refresh %v", snaps))
	affected, tss, err := snapstate.UpdateMany(context.Background(), s.state, snaps, nil, s.user.ID, &snapstate.Flags{NoReRefresh: true})
	c.Assert(err, IsNil)
	c.Assert(affected, testutil.DeepUnsortedMatches, snaps)

	for _, ts := range tss {
		chg.AddAll(ts)
	}

	// up until daemon restart caused by snapd installation
	s.settle(c)
	kind := restart.Pending(s.state)
	c.Check(kind, Equals, restart.RestartDaemon)
	restart.MockPending(s.state, restart.RestartUnset)
	// run through the end
	s.settle(c)

	// the plug waits for the default provider which in turn waits for core18 to complete its link-snap (but not reboot)
	// that needs to wait for a reboot (not tested here, but in ifacestate)
	for _, snap := range []string{"snapd", "snap-content-plug", "snap-content-slot"} {
		t := findTaskForSnap(c, chg, "auto-connect", snap)
		c.Assert(t.Status(), Equals, state.DoneStatus, Commentf("expected task %q for snap %q to be done: %s", t.Kind(), snap, t.Status()))
	}

	// waiting for reboot
	for _, snap := range []string{"kernel", "gadget", "core18"} {
		t := findTaskForSnap(c, chg, "auto-connect", snap)
		c.Assert(t.Status(), Equals, state.DoStatus, Commentf("expected task %q for snap %q to be do: %s", t.Kind(), snap, t.Status()))
	}

	t := findTaskForSnap(c, chg, "link-snap", "kernel")
	c.Assert(t.Status(), Equals, state.WaitStatus, Commentf("expected kernel's link-snap to be waiting for restart"))

	s.mockRestartAndSettle(c, chg)

	// completed after restart
	for _, snap := range []string{"kernel", "gadget", "core18", "snap-content-slot"} {
		t := findTaskForSnap(c, chg, "auto-connect", snap)
		c.Assert(t.Status(), Equals, state.DoneStatus, Commentf("expected task %q for snap %q to be done: %s", t.Kind(), snap, t.Status()))
	}

	c.Check(chg.IsReady(), Equals, true)
	c.Check(chg.Status(), Equals, state.DoneStatus)
}

func (s *snapmgrTestSuite) TestAutoRefreshSplitRefresh(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	restore := release.MockOnClassic(true)
	defer restore()

	restore = snapstatetest.MockDeviceModel(ModelWithBase("core18"))
	defer restore()

	restore = snapstate.MockPrerequisitesRetryTimeout(200 * time.Millisecond)
	defer restore()

	snaps := []string{"snapd", "kernel", "core18", "gadget", "some-snap-with-core18-base"}
	types := map[string]string{
		"snapd":                      "snapd",
		"core18":                     "base",
		"gadget":                     "gadget",
		"kernel":                     "kernel",
		"some-snap-with-core18-base": "app",
	}
	snapIds := map[string]string{
		"snapd":                      "snapd-snap-id",
		"core18":                     "core18-snap-id",
		"gadget":                     "gadget-core18-id",
		"kernel":                     "kernel-id",
		"some-snap-with-core18-base": "some-snap-with-core18-base",
	}
	bases := map[string]string{
		// create a dependency between the two task sets
		"some-snap-with-core18-base": "core18",
	}

	chg := s.state.NewChange("auto-refresh", "test change")
	task := s.state.NewTask("conditional-auto-refresh", "mock conditional auto refresh task")
	chg.AddTask(task)

	cands := make(map[string]*snapstate.RefreshCandidate, len(snaps))
	for _, sn := range snaps {
		yaml := fmt.Sprintf("name: %s\nversion: 1.0\nepoch: 1\ntype: %s", sn, types[sn])
		if base, ok := bases[sn]; ok {
			yaml += fmt.Sprintf("\nbase: %s", base)
		}

		oldSi := &snap.SideInfo{RealName: sn, SnapID: snapIds[sn], Revision: snap.R(1)}
		newSi := &snap.SideInfo{RealName: sn, SnapID: snapIds[sn], Revision: snap.R(11)}

		snaptest.MakeTestSnapInfoWithFiles(c, yaml, nil, newSi)

		snaptest.MockSnap(c, yaml, oldSi)
		snapstate.Set(s.state, sn, &snapstate.SnapState{
			Active:          true,
			Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{oldSi}),
			Current:         oldSi.Revision,
			TrackingChannel: "latest/stable",
			SnapType:        types[sn],
		})

		cands[sn] = &snapstate.RefreshCandidate{
			SnapSetup: snapstate.SnapSetup{
				SideInfo: newSi,
				Version:  sn + "Ver",
				Base:     bases[sn],
				Type:     snap.Type(types[sn]),
				Flags:    snapstate.Flags{IsAutoRefresh: true},
			},
		}
	}

	s.fakeBackend.linkSnapRebootFor = map[string]bool{
		"core18": true,
		"kernel": true,
		"gadget": true,
	}
	s.fakeBackend.linkSnapMaybeReboot = true

	s.o.TaskRunner().AddHandler("update-gadget-assets",
		func(task *state.Task, tomb *tomb.Tomb) error {
			task.State().Lock()
			defer task.State().Unlock()
			chg := task.Change()
			chg.Set("gadget-restart-required", true)
			return nil
		},
		func(task *state.Task, tomb *tomb.Tomb) error { return nil })

	s.o.TaskRunner().AddHandler("update-gadget-cmdline",
		func(task *state.Task, tomb *tomb.Tomb) error { return nil },
		func(task *state.Task, tomb *tomb.Tomb) error { return nil })

	task.Set("snaps", cands)

	// run til daemon restart
	s.settle(c)

	kind := restart.Pending(s.state)
	c.Check(kind, Equals, restart.RestartDaemon)
	restart.MockPending(s.state, restart.RestartUnset)

	// run until kernel reboot
	s.settle(c)

	// some-snap-with-core18-base depends on the base but the prereq code only waits
	// for the base link-snap so it can complete before the reboot
	for _, snap := range []string{"snapd", "some-snap-with-core18-base"} {
		t := findTaskForSnap(c, chg, "auto-connect", snap)
		c.Assert(t.Status(), Equals, state.DoneStatus, Commentf("expected task %q for %q to be \"Done\": %s", t.Kind(), snap, t.Status()))
	}

	for _, snap := range []string{"kernel", "gadget", "core18"} {
		t := findTaskForSnap(c, chg, "auto-connect", snap)
		c.Assert(t.Status(), Equals, state.DoStatus, Commentf("expected task %q for %q to be \"Do\": %s", t.Kind(), snap, t.Status()))
	}

	// check that the rerefresh task is done because the essential tasks are
	// ignored
	rerefreshTask := findLastTask(chg, "check-rerefresh")
	c.Assert(rerefreshTask, NotNil, Commentf("cannot find check-rerefresh task"))
	c.Assert(rerefreshTask.Status(), Equals, state.DoneStatus)

	t := findTaskForSnap(c, chg, "link-snap", "kernel")
	c.Assert(t.Status(), Equals, state.WaitStatus, Commentf("expected kernel's link-snap to be waiting for restart"))
	s.mockRestartAndSettle(c, chg)

	for _, snap := range []string{"kernel", "gadget", "core18", "some-snap-with-core18-base"} {
		t := findTaskForSnap(c, chg, "auto-connect", snap)
		c.Assert(t.Status(), Equals, state.DoneStatus, Commentf("expected task %q for %q to be in \"Done\": %s", t.Kind(), snap, t.Status()))
	}

	c.Check(chg.IsReady(), Equals, true)
	c.Check(chg.Status(), Equals, state.DoneStatus)
}

func (s *snapmgrTestSuite) TestRefreshWithConfdb(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:          true,
		TrackingChannel: "latest/edge",
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(7)}}),
		Current:         snap.R(7),
		SnapType:        "app",
	})

	chg := s.state.NewChange("test", "test change")
	ts, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "channel-for-confdb"}, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)
	chg.AddAll(ts)

	checkSnapsupHasConfdb(ts, c)
}

func checkSnapsupHasConfdb(ts *state.TaskSet, c *C) {
	tasks := tasksWithKind(ts, "validate-snap")
	c.Assert(tasks, HasLen, 1)

	snapsup, err := snapstate.TaskSnapSetup(tasks[0])
	c.Assert(err, IsNil)
	c.Assert(snapsup.PluggedConfdbIDs, DeepEquals, []confdb.SchemaID{{Account: "my-publisher", Name: "my-reg"}})
}

func findTaskForSnap(c *C, chg *state.Change, kind, snap string) *state.Task {
	for _, t := range chg.Tasks() {
		if t.Kind() != kind {
			continue
		}

		snapsup, err := snapstate.TaskSnapSetup(t)
		c.Assert(err, IsNil)
		if snapsup.SnapName() == snap {
			return t
		}
	}

	c.Fatalf("couldn't find %q task for %q", kind, snap)
	return nil
}

func (s *snapmgrTestSuite) TestUpdateStateConflict(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:          true,
		TrackingChannel: "latest/stable",
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(2)},
		}),
		Current:  snap.R(2),
		SnapType: "app",
	})

	snapstate.ReplaceStore(s.state, sneakyStore{fakeStore: s.fakeStore, state: s.state})

	_, err := snapstate.Update(s.state, "some-snap", nil, 0, snapstate.Flags{})
	c.Check(err, testutil.ErrorIs, &snapstate.ChangeConflictError{})
	c.Assert(err, ErrorMatches, `snap "some-snap" has changes in progress`)
}

func (s *snapmgrTestSuite) TestUpdateStateConflictRemoved(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:          true,
		TrackingChannel: "latest/stable",
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(2)},
		}),
		Current:  snap.R(2),
		SnapType: "app",
	})

	snapstate.ReplaceStore(s.state, sneakyStore{fakeStore: s.fakeStore, state: s.state, remove: true})

	_, err := snapstate.Update(s.state, "some-snap", nil, 0, snapstate.Flags{})
	c.Check(err, testutil.ErrorIs, &snapstate.ChangeConflictError{})
	c.Assert(err, ErrorMatches, `snap "some-snap" has changes in progress`)
}

func (s *snapmgrTestSuite) TestUpdateBackToPrevRevision(c *C) {
	const (
		snapName    = "some-snap"
		instanceKey = "key"
		snapID      = "some-snap-id"
		channel     = "some-channel"
	)

	currentSnapRev := snap.R(11)
	prevSnapRev := snap.R(7)
	instanceName := snap.InstanceName(snapName, instanceKey)

	s.fakeStore.snapResourcesFn = func(info *snap.Info) []store.SnapResourceResult {
		c.Fatalf("unexpected call to snapResourcesFn")
		return nil
	}

	// we start without the auxiliary store info (or with an older one)
	c.Check(backend.AuxStoreInfoFilename(snapID), testutil.FileAbsent)

	currentSI := snap.SideInfo{
		RealName: snapName,
		Revision: currentSnapRev,
		SnapID:   snapID,
		Channel:  channel,
	}
	snaptest.MockSnapInstance(c, instanceName, fmt.Sprintf("name: %s", snapName), &currentSI)

	restore := snapstate.MockRevisionDate(nil)
	defer restore()

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

	// since we don't have any components to check on, we shouldn't hit the
	// store at all
	snapstate.ReplaceStore(s.state, &storetest.Store{})

	if instanceKey != "" {
		tr := config.NewTransaction(s.state)
		tr.Set("core", "experimental.parallel-instances", true)
		tr.Commit()
	}

	prevSI := snap.SideInfo{
		RealName: snapName,
		Revision: prevSnapRev,
		SnapID:   snapID,
		Channel:  channel,
	}

	seq := snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&prevSI, &currentSI})

	snapstate.Set(s.state, instanceName, &snapstate.SnapState{
		Active:          true,
		Sequence:        seq,
		Current:         currentSI.Revision,
		SnapType:        "app",
		TrackingChannel: channel,
		InstanceKey:     instanceKey,
	})

	ts, err := snapstate.Update(s.state, instanceName, &snapstate.RevisionOptions{
		Revision: prevSnapRev,
	}, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)

	chg := s.state.NewChange("refresh", "refresh a snap")
	chg.AddAll(ts)

	// check unlink-reason
	unlinkTask := findLastTask(chg, "unlink-current-snap")
	c.Assert(unlinkTask, NotNil)
	var unlinkReason string
	unlinkTask.Get("unlink-reason", &unlinkReason)
	c.Check(unlinkReason, Equals, "refresh")

	// local modifications, edge must be set
	te := ts.MaybeEdge(snapstate.LastBeforeLocalModificationsEdge)
	c.Assert(te, NotNil)
	c.Assert(te.Kind(), Equals, "prepare-snap")

	s.settle(c)

	c.Assert(chg.Err(), IsNil, Commentf("change tasks:\n%s", printTasks(chg.Tasks())))

	expected := fakeOps{
		{
			op:   "remove-snap-aliases",
			name: instanceName,
		},
		{
			op:          "run-inhibit-snap-for-unlink",
			name:        instanceName,
			inhibitHint: "refresh",
		},
		{
			op:   "discard-namespace-locked",
			name: instanceName,
		},
		{
			op:          "unlink-snap",
			path:        filepath.Join(dirs.SnapMountDir, instanceName, currentSnapRev.String()),
			inhibitHint: "refresh",
		},
		{
			op:   "copy-data",
			path: filepath.Join(dirs.SnapMountDir, instanceName, prevSnapRev.String()),
			old:  filepath.Join(dirs.SnapMountDir, instanceName, currentSnapRev.String()),
		},
		{
			op:   "setup-snap-save-data",
			path: filepath.Join(dirs.SnapDataSaveDir, instanceName),
		},
		{
			op:    "setup-profiles:Doing",
			name:  instanceName,
			revno: prevSnapRev,
		},
		{
			op: "candidate",
			sinfo: snap.SideInfo{
				RealName: snapName,
				SnapID:   snapID,
				Channel:  channel,
				Revision: prevSnapRev,
			},
		},
		{
			op:   "link-snap",
			path: filepath.Join(dirs.SnapMountDir, instanceName, prevSnapRev.String()),
		},
		{
			op: "maybe-set-next-boot",
		},
		{
			op:    "auto-connect:Doing",
			name:  instanceName,
			revno: prevSnapRev,
		},
		{
			op: "update-aliases",
		},
		{
			op:    "cleanup-trash",
			name:  instanceName,
			revno: prevSnapRev,
		},
	}

	// start with an easier-to-read error if this fails:
	c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
	c.Assert(s.fakeBackend.ops, DeepEquals, expected)

	task := ts.Tasks()[1]

	// verify snapSetup info
	var snapsup snapstate.SnapSetup
	err = task.Get("snap-setup", &snapsup)
	c.Assert(err, IsNil)
	c.Assert(snapsup, DeepEquals, snapstate.SnapSetup{
		Channel: channel,
		UserID:  s.user.ID,

		SnapPath:  filepath.Join(dirs.SnapBlobDir, fmt.Sprintf("%s_%v.snap", instanceName, prevSnapRev)),
		SideInfo:  snapsup.SideInfo,
		Type:      snap.TypeApp,
		Version:   "some-snapVer",
		PlugsOnly: true,
		Flags: snapstate.Flags{
			Transaction: client.TransactionPerSnap,
		},
		InstanceKey:                     instanceKey,
		PreUpdateKernelModuleComponents: []*snap.ComponentSideInfo{},
	})
	c.Assert(snapsup.SideInfo, DeepEquals, &snap.SideInfo{
		RealName: snapName,
		Revision: prevSnapRev,
		Channel:  channel,
		SnapID:   snapID,
	})

	// verify snaps in the system state
	var snapst snapstate.SnapState
	err = snapstate.Get(s.state, instanceName, &snapst)
	c.Assert(err, IsNil)

	c.Assert(snapst.LastRefreshTime, NotNil)
	c.Assert(snapst.Active, Equals, true)
	c.Assert(snapst.Sequence.Revisions, HasLen, 2)

	// link-snap should put the revision we refreshed to at the end of the
	// sequence. in this case, swapping their positions.
	c.Assert(snapst.Sequence.Revisions[1], DeepEquals, seq.Revisions[0])
	c.Assert(snapst.Sequence.Revisions[0], DeepEquals, seq.Revisions[1])
}

func (s *snapmgrTestSuite) TestRevertWithComponents(c *C) {
	const undo = false
	s.testRevertWithComponents(c, undo)
}

func (s *snapmgrTestSuite) TestRevertWithComponentsUndo(c *C) {
	const undo = true
	s.testRevertWithComponents(c, undo)
}

func (s *snapmgrTestSuite) testRevertWithComponents(c *C, undo bool) {
	const (
		snapName    = "some-snap"
		instanceKey = "key"
		snapID      = "some-snap-id"
		channel     = "channel-for-components"
	)

	components := []string{"standard-component", "standard-component-extra"}

	currentSnapRev := snap.R(11)
	prevSnapRev := snap.R(7)
	instanceName := snap.InstanceName(snapName, instanceKey)

	sort.Strings(components)

	s.fakeStore.snapResourcesFn = func(info *snap.Info) []store.SnapResourceResult {
		c.Fatalf("unexpected call to snapResourcesFn")
		return nil
	}

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

	prevSI := snap.SideInfo{
		RealName: snapName,
		Revision: prevSnapRev,
		SnapID:   snapID,
		Channel:  channel,
	}
	currentSI := snap.SideInfo{
		RealName: snapName,
		Revision: currentSnapRev,
		SnapID:   snapID,
		Channel:  channel,
	}
	seq := snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&prevSI, &currentSI})

	currentKmodComps := make([]*snap.ComponentSideInfo, 0, len(components))
	prevKmodComps := make([]*snap.ComponentSideInfo, 0, len(components))
	for i, comp := range components {
		prevCsi := snap.ComponentSideInfo{
			Component: naming.NewComponentRef(snapName, comp),
			Revision:  snap.R(i + 1),
		}
		err := seq.AddComponentForRevision(prevSnapRev, &sequence.ComponentState{
			SideInfo: &prevCsi,
			CompType: componentNameToType(c, comp),
		})
		c.Assert(err, IsNil)

		currentCsi := snap.ComponentSideInfo{
			Component: naming.NewComponentRef(snapName, comp),
			Revision:  snap.R(i + 2),
		}
		err = seq.AddComponentForRevision(currentSnapRev, &sequence.ComponentState{
			SideInfo: &currentCsi,
			CompType: componentNameToType(c, comp),
		})
		c.Assert(err, IsNil)

		if strings.HasPrefix(comp, string(snap.KernelModulesComponent)) {
			prevKmodComps = append(prevKmodComps, &prevCsi)
			currentKmodComps = append(currentKmodComps, &currentCsi)
		}
	}

	s.AddCleanup(snapstate.MockReadComponentInfo(func(
		compMntDir string, info *snap.Info, csi *snap.ComponentSideInfo,
	) (*snap.ComponentInfo, error) {
		return &snap.ComponentInfo{
			Component:         csi.Component,
			Type:              componentNameToType(c, csi.Component.ComponentName),
			CompVersion:       "1.0",
			ComponentSideInfo: *csi,
		}, nil
	}))

	snapstate.Set(s.state, instanceName, &snapstate.SnapState{
		Active:          true,
		Sequence:        seq,
		Current:         currentSI.Revision,
		SnapType:        "app",
		TrackingChannel: channel,
		InstanceKey:     instanceKey,
	})

	ts, err := snapstate.Revert(s.state, instanceName, snapstate.Flags{}, "")
	c.Assert(err, IsNil)

	chg := s.state.NewChange("revert", "revert a snap")
	chg.AddAll(ts)

	if undo {
		last := ts.Tasks()[len(ts.Tasks())-1]

		terr := s.state.NewTask("error-trigger", "provoking total undo")
		terr.WaitFor(last)
		chg.AddTask(terr)
	}

	// local modifications, edge must be set
	te := ts.MaybeEdge(snapstate.LastBeforeLocalModificationsEdge)
	c.Assert(te, NotNil)
	c.Assert(te.Kind(), Equals, "validate-component")

	s.settle(c)

	if undo {
		c.Assert(chg.Err(), NotNil, Commentf("change tasks:\n%s", printTasks(chg.Tasks())))
	} else {
		c.Assert(chg.Err(), IsNil, Commentf("change tasks:\n%s", printTasks(chg.Tasks())))
	}

	var expected fakeOps
	for _, cs := range seq.ComponentsForRevision(prevSnapRev) {
		expected = append(expected, fakeOp{
			op:                              "validate-component:Doing",
			name:                            instanceName,
			revno:                           prevSnapRev,
			componentName:                   cs.SideInfo.Component.ComponentName,
			componentRev:                    cs.SideInfo.Revision,
			componentSideInfo:               *cs.SideInfo,
			componentSkipAssertionsDownload: true,
		})
	}

	// note the absence of link-component tasks. that is because we do not need
	// to link components when reverting, since they should already be linked,
	// as they were part of a the previous revision
	expected = append(expected, fakeOps{
		{
			op:   "remove-snap-aliases",
			name: instanceName,
		},
		{
			op:          "run-inhibit-snap-for-unlink",
			name:        instanceName,
			inhibitHint: "refresh",
		},
		{
			op:   "discard-namespace-locked",
			name: instanceName,
		},
		{
			op:          "unlink-snap",
			path:        filepath.Join(dirs.SnapMountDir, instanceName, currentSnapRev.String()),
			inhibitHint: "refresh",
		},
		{
			op:    "setup-profiles:Doing",
			name:  instanceName,
			revno: prevSnapRev,
		},
		{
			op: "candidate",
			sinfo: snap.SideInfo{
				RealName: snapName,
				SnapID:   snapID,
				Channel:  channel,
				Revision: prevSnapRev,
			},
		},
		{
			op:   "link-snap",
			path: filepath.Join(dirs.SnapMountDir, instanceName, prevSnapRev.String()),
		},
		{
			op: "maybe-set-next-boot",
		},
		{
			op:    "auto-connect:Doing",
			name:  instanceName,
			revno: prevSnapRev,
		},
		{
			op: "update-aliases",
		},
	}...)

	if undo {
		expected = append(expected, []fakeOp{
			{
				op:   "remove-snap-aliases",
				name: instanceName,
			},
			{
				op:    "auto-connect:Undoing",
				name:  instanceName,
				revno: prevSnapRev,
			},
			{
				op:   "unlink-snap",
				path: filepath.Join(dirs.SnapMountDir, instanceName, prevSnapRev.String()),
			},
			{
				op:    "setup-profiles:Undoing",
				name:  instanceName,
				revno: prevSnapRev,
			},
			{
				op:   "link-snap",
				path: filepath.Join(dirs.SnapMountDir, instanceName, currentSnapRev.String()),
			},
			{
				op:     "maybe-set-next-boot",
				isUndo: true,
			},
			{
				op: "update-aliases",
			},
		}...)
	}

	// start with an easier-to-read error if this fails:
	c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
	c.Assert(s.fakeBackend.ops, DeepEquals, expected)

	task := ts.Tasks()[1]

	// verify snapSetup info
	var snapsup snapstate.SnapSetup
	err = task.Get("snap-setup", &snapsup)
	c.Assert(err, IsNil)
	c.Assert(snapsup, DeepEquals, snapstate.SnapSetup{
		SideInfo:  snapsup.SideInfo,
		Type:      snap.TypeApp,
		Version:   "some-snapVer",
		PlugsOnly: true,
		Flags: snapstate.Flags{
			Revert: true,
		},
		InstanceKey:                     instanceKey,
		PreUpdateKernelModuleComponents: currentKmodComps,
	})
	c.Assert(snapsup.SideInfo, DeepEquals, &snap.SideInfo{
		RealName: snapName,
		Revision: prevSnapRev,
		Channel:  channel,
		SnapID:   snapID,
	})

	// verify snaps in the system state
	var snapst snapstate.SnapState
	err = snapstate.Get(s.state, instanceName, &snapst)
	c.Assert(err, IsNil)

	c.Assert(snapst.Active, Equals, true)
	c.Assert(snapst.Sequence.Revisions, HasLen, 2)

	if undo {
		c.Assert(snapst.Current, Equals, currentSnapRev)
	} else {
		c.Assert(snapst.Current, Equals, prevSnapRev)
	}

	// revert maintains the order of the sequence
	c.Assert(snapst.Sequence.Revisions, DeepEquals, seq.Revisions)
}

func (s *snapmgrTestSuite) TestUpdateWithComponentsBackToPrevRevision(c *C) {
	const (
		snapName = "kernel-snap-with-components"
		snapID   = "kernel-snap-with-components-id"
		channel  = "channel-for-components-only-component-refresh"
	)

	r := snapstatetest.MockDeviceModel(MakeModel20("pc", map[string]any{"base": "core24"}))
	defer r()

	components := []string{"standard-component", "kernel-modules-component"}

	currentSnapRev := snap.R(11)
	prevSnapRev := snap.R(7)

	sort.Strings(components)

	// we start without the auxiliary store info (or with an older one)
	c.Check(backend.AuxStoreInfoFilename(snapID), testutil.FileAbsent)

	currentSI := snap.SideInfo{
		RealName: snapName,
		Revision: currentSnapRev,
		SnapID:   snapID,
		Channel:  channel,
	}
	snaptest.MockSnap(c, fmt.Sprintf("name: %s\ntype: kernel\n", snapName), &currentSI)

	restore := snapstate.MockRevisionDate(nil)
	defer restore()

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

	prevSI := snap.SideInfo{
		RealName: snapName,
		Revision: prevSnapRev,
		SnapID:   snapID,
		Channel:  channel,
	}

	otherSI := snap.SideInfo{
		RealName: snapName,
		Revision: snap.R(99),
		SnapID:   snapID,
		Channel:  channel,
	}

	seq := snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&otherSI, &prevSI, &currentSI})

	currentKmodComps := make([]*snap.ComponentSideInfo, 0, len(components))
	newKmodComps := make([]*snap.ComponentSideInfo, 0, len(components))
	currentResources := make(map[string]snap.Revision, len(components))

	// we have three sets of revisions we are working with:
	// 1. current component revisions (+1 to index)
	// 2. previous component revisions (+3 to index)
	// 3. new component revisions (that is connected with the original snap
	//    revision, via the store) (+2 to index)
	for i, comp := range components {
		prevCsi := snap.ComponentSideInfo{
			Component: naming.NewComponentRef(snapName, comp),
			Revision:  snap.R(i + 1),
		}

		err := seq.AddComponentForRevision(prevSnapRev, &sequence.ComponentState{
			SideInfo: &prevCsi,
			CompType: componentNameToType(c, comp),
		})
		c.Assert(err, IsNil)

		currentCsi := snap.ComponentSideInfo{
			Component: naming.NewComponentRef(snapName, comp),
			Revision:  snap.R(i + 3),
		}
		err = seq.AddComponentForRevision(currentSnapRev, &sequence.ComponentState{
			SideInfo: &currentCsi,
			CompType: componentNameToType(c, comp),
		})
		c.Assert(err, IsNil)

		if strings.HasPrefix(comp, string(snap.KernelModulesComponent)) {
			newKmodComps = append(newKmodComps, &snap.ComponentSideInfo{
				Component: naming.NewComponentRef(snapName, comp),
				Revision:  snap.R(i + 2),
			})
			currentKmodComps = append(currentKmodComps, &currentCsi)
		}
		currentResources[comp] = snap.R(i + 3)
	}

	availableComponents := make([]string, len(components))
	copy(availableComponents, components)
	availableComponents = append(availableComponents, "standard-component-extra", "standard-component-present-in-sequence")

	// standard-component-extra is installed for just the revision we're moving to,
	// it should be unlinked and discarded
	extraCsi := snap.ComponentSideInfo{
		Component: naming.NewComponentRef(snapName, "standard-component-extra"),
		Revision:  snap.R(len(availableComponents) + 1),
	}
	err := seq.AddComponentForRevision(prevSnapRev, &sequence.ComponentState{
		SideInfo: &extraCsi,
		CompType: componentNameToType(c, extraCsi.Component.ComponentName),
	})
	c.Assert(err, IsNil)

	// standard-component-present-in-sequence is installed for the revision we're
	// moving to and another revision. it should be unlinked, but not discarded.
	presentInSeqCsi := snap.ComponentSideInfo{
		Component: naming.NewComponentRef(snapName, "standard-component-present-in-sequence"),
		Revision:  snap.R(len(availableComponents) + 1),
	}
	for _, si := range []*snap.SideInfo{&prevSI, &otherSI} {
		err := seq.AddComponentForRevision(si.Revision, &sequence.ComponentState{
			SideInfo: &presentInSeqCsi,
			CompType: componentNameToType(c, presentInSeqCsi.Component.ComponentName),
		})
		c.Assert(err, IsNil)
	}

	s.fakeStore.snapResourcesFn = func(info *snap.Info) []store.SnapResourceResult {
		c.Assert(info.InstanceName(), DeepEquals, snapName)
		var results []store.SnapResourceResult
		for i, compName := range availableComponents {
			results = append(results, store.SnapResourceResult{
				DownloadInfo: snap.DownloadInfo{
					DownloadURL: "http://example.com/" + compName,
				},
				Name:      compName,
				Revision:  i + 2,
				Type:      fmt.Sprintf("component/%s", componentNameToType(c, compName)),
				Version:   "1.0",
				CreatedAt: "2024-01-01T00:00:00Z",
			})
		}
		return results
	}

	s.AddCleanup(snapstate.MockReadComponentInfo(func(
		compMntDir string, info *snap.Info, csi *snap.ComponentSideInfo,
	) (*snap.ComponentInfo, error) {
		return &snap.ComponentInfo{
			Component:         csi.Component,
			Type:              componentNameToType(c, csi.Component.ComponentName),
			CompVersion:       "1.0",
			ComponentSideInfo: *csi,
		}, nil
	}))

	snapstate.Set(s.state, snapName, &snapstate.SnapState{
		Active:          true,
		Sequence:        seq,
		Current:         currentSI.Revision,
		SnapType:        "kernel",
		TrackingChannel: channel,
	})

	ts, err := snapstate.Update(s.state, snapName, &snapstate.RevisionOptions{
		Revision: prevSnapRev,
	}, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)

	chg := s.state.NewChange("refresh", "refresh a snap")
	chg.AddAll(ts)

	// check unlink-reason
	unlinkTask := findLastTask(chg, "unlink-current-snap")
	c.Assert(unlinkTask, NotNil)
	var unlinkReason string
	unlinkTask.Get("unlink-reason", &unlinkReason)
	c.Check(unlinkReason, Equals, "refresh")

	// local modifications, edge must be set
	te := ts.MaybeEdge(snapstate.LastBeforeLocalModificationsEdge)
	c.Assert(te, NotNil)
	c.Assert(te.Kind(), Equals, "validate-component")

	s.settle(c)

	c.Assert(chg.Err(), IsNil, Commentf("change tasks:\n%s", printTasks(chg.Tasks())))

	fi, err := os.Stat(snap.MountFile(snapName, currentSnapRev))
	c.Assert(err, IsNil)

	expected := fakeOps{
		{
			op: "storesvc-snap-action",
			curSnaps: []store.CurrentSnap{{
				InstanceName:    snapName,
				SnapID:          snapID,
				Revision:        currentSnapRev,
				TrackingChannel: channel,
				RefreshedDate:   fi.ModTime(),
				Epoch:           snap.E("1*"),
				Resources:       currentResources,
			}},
			userID: 1,
		},
		{
			op: "storesvc-snap-action:action",
			action: store.SnapAction{
				Action:          "refresh",
				InstanceName:    snapName,
				Revision:        prevSnapRev,
				SnapID:          snapID,
				ResourceInstall: true,
				// note that channel is empty here since we can't provide a
				// channel in this case, since we don't know if the revision is
				// a part of the channel
				Channel: "",
			},
			revno:  prevSnapRev,
			userID: 1,
		},
	}

	for i, compName := range components {
		csi := snap.ComponentSideInfo{
			Component: naming.NewComponentRef(snapName, compName),
			Revision:  snap.R(i + 2),
		}

		filename := fmt.Sprintf("%s_%v.comp", csi.Component, csi.Revision)

		expected = append(expected, []fakeOp{{
			op:   "storesvc-download",
			name: csi.Component.String(),
		}, {
			op:                "validate-component:Doing",
			name:              snapName,
			revno:             prevSnapRev,
			componentName:     compName,
			componentPath:     filepath.Join(dirs.SnapBlobDir, filename),
			componentRev:      csi.Revision,
			componentSideInfo: csi,
		}}...)
	}

	for _, unlinked := range []snap.ComponentSideInfo{extraCsi, presentInSeqCsi} {
		expected = append(expected, fakeOp{
			op:   "unlink-component",
			path: snap.ComponentMountDir(unlinked.Component.ComponentName, unlinked.Revision, snapName),
		})
	}

	for i, compName := range components {
		csi := snap.ComponentSideInfo{
			Component: naming.NewComponentRef(snapName, compName),
			Revision:  snap.R(i + 2),
		}

		filename := fmt.Sprintf("%s_%v.comp", csi.Component, csi.Revision)

		expected = append(expected, fakeOp{
			op:                "setup-component",
			containerName:     csi.Component.String(),
			containerFileName: filename,
		})
	}

	expected = append(expected, fakeOp{
		op:   "remove-snap-aliases",
		name: snapName,
	})

	expected = append(expected, fakeOps{
		{
			op:          "run-inhibit-snap-for-unlink",
			name:        snapName,
			inhibitHint: "refresh",
		},
		{
			op:   "discard-namespace-locked",
			name: snapName,
		},
		{
			op:          "unlink-snap",
			path:        filepath.Join(dirs.SnapMountDir, snapName, currentSnapRev.String()),
			inhibitHint: "refresh",
		},
		{
			op: "prepare-kernel-snap",
		},
		{
			op:    "update-gadget-assets:Doing",
			name:  "kernel-snap-with-components",
			revno: prevSnapRev,
		},
		{
			op:   "copy-data",
			path: filepath.Join(dirs.SnapMountDir, snapName, prevSnapRev.String()),
			old:  filepath.Join(dirs.SnapMountDir, snapName, currentSnapRev.String()),
		},
		{
			op:   "setup-snap-save-data",
			path: filepath.Join(dirs.SnapDataSaveDir, snapName),
		},
		{
			op:    "setup-profiles:Doing",
			name:  snapName,
			revno: prevSnapRev,
		},
		{
			op: "candidate",
			sinfo: snap.SideInfo{
				RealName: snapName,
				SnapID:   snapID,
				Channel:  channel,
				Revision: prevSnapRev,
			},
		},
		{
			op:                  "link-snap",
			path:                filepath.Join(dirs.SnapMountDir, snapName, prevSnapRev.String()),
			requireSnapdTooling: true,
		},
	}...)

	for i, compName := range components {
		expected = append(expected, fakeOp{
			op:   "link-component",
			path: snap.ComponentMountDir(compName, snap.R(i+2), snapName),
		})
	}

	expected = append(expected, fakeOps{
		{
			op:    "auto-connect:Doing",
			name:  snapName,
			revno: prevSnapRev,
		},
		{
			op: "update-aliases",
		},
	}...)

	if len(newKmodComps) > 0 || len(currentKmodComps) > 0 {
		expected = append(expected, fakeOp{
			op:           "prepare-kernel-modules-components",
			currentComps: currentKmodComps,
			finalComps:   newKmodComps,
		}, fakeOp{
			op: "maybe-set-next-boot",
		})
	}

	// note that test-present-in-both-component is not discarded since it is
	// still referenced by the original snap revision
	discardedContainerName := fmt.Sprintf("%s+%s", snapName, extraCsi.Component.ComponentName)
	discardedFilename := fmt.Sprintf("%s_%v.comp", discardedContainerName, extraCsi.Revision)
	expected = append(expected, []fakeOp{
		{
			op: "remove-kernel-snap-setup",
		},
		{
			op:                "undo-setup-component",
			containerName:     discardedContainerName,
			containerFileName: discardedFilename,
		},
		{
			op:                "remove-component-dir",
			containerName:     discardedContainerName,
			containerFileName: discardedFilename,
		},
	}...)

	expected = append(expected, fakeOp{
		op:    "cleanup-trash",
		name:  snapName,
		revno: prevSnapRev,
	})

	// start with an easier-to-read error if this fails:
	c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
	c.Assert(s.fakeBackend.ops, DeepEquals, expected)

	task := ts.Tasks()[1]

	// verify snapSetup info
	var snapsup snapstate.SnapSetup
	err = task.Get("snap-setup", &snapsup)
	c.Assert(err, IsNil)
	c.Assert(snapsup, DeepEquals, snapstate.SnapSetup{
		Channel: channel,
		UserID:  s.user.ID,

		SnapPath:  filepath.Join(dirs.SnapBlobDir, fmt.Sprintf("%s_%v.snap", snapName, prevSnapRev)),
		SideInfo:  snapsup.SideInfo,
		Type:      snap.TypeKernel,
		Version:   "kernel-snap-with-componentsVer",
		PlugsOnly: true,
		Flags: snapstate.Flags{
			Transaction: client.TransactionPerSnap,
		},
		PreUpdateKernelModuleComponents: currentKmodComps,
	})
	c.Assert(snapsup.SideInfo, DeepEquals, &snap.SideInfo{
		RealName: snapName,
		Revision: prevSnapRev,
		Channel:  channel,
		SnapID:   snapID,
	})

	// verify snaps in the system state
	var snapst snapstate.SnapState
	err = snapstate.Get(s.state, snapName, &snapst)
	c.Assert(err, IsNil)

	c.Assert(snapst.LastRefreshTime, NotNil)
	c.Assert(snapst.Active, Equals, true)
	c.Assert(snapst.Sequence.Revisions, HasLen, 3)

	// the original revision's components should be replaced with the new
	// components
	seq.Revisions[1].Components = nil
	for i, comp := range components {
		err := seq.AddComponentForRevision(prevSnapRev, &sequence.ComponentState{
			SideInfo: &snap.ComponentSideInfo{
				Component: naming.NewComponentRef(snapName, comp),
				Revision:  snap.R(i + 2),
			},
			CompType: componentNameToType(c, comp),
		})
		c.Assert(err, IsNil)
	}

	// link-snap should put the revision we refreshed to at the end of the
	// sequence
	c.Assert(snapst.Sequence.Revisions[2], DeepEquals, seq.Revisions[1])
	c.Assert(snapst.Sequence.Revisions[1], DeepEquals, seq.Revisions[2])
	c.Assert(snapst.Sequence.Revisions[0], DeepEquals, seq.Revisions[0])
}

func (s *snapmgrTestSuite) TestUpdateWithComponentsBackToPrevRevisionAlreadyPresent(c *C) {
	s.testUpdateWithComponentsBackToPrevRevisionAlreadyPresent(c, false)
}

func (s *snapmgrTestSuite) TestUpdateWithComponentsBackToPrevRevisionAlreadyPresentUndo(c *C) {
	s.testUpdateWithComponentsBackToPrevRevisionAlreadyPresent(c, true)
}

func (s *snapmgrTestSuite) testUpdateWithComponentsBackToPrevRevisionAlreadyPresent(c *C, undo bool) {
	const (
		snapName = "kernel-snap-with-components"
		snapID   = "kernel-snap-with-components-id"
		channel  = "channel-for-components-only-component-refresh"
	)

	restore := snapstatetest.MockDeviceModel(MakeModel20("pc", map[string]any{"base": "core24"}))
	defer restore()

	components := []string{"standard-component", "kernel-modules-component"}
	currentSnapRev := snap.R(11)
	prevSnapRev := snap.R(7)

	sort.Strings(components)

	currentSI := snap.SideInfo{
		RealName: snapName,
		Revision: currentSnapRev,
		SnapID:   snapID,
		Channel:  channel,
	}
	snaptest.MockSnap(c, fmt.Sprintf("name: %s\ntype: kernel\n", snapName), &currentSI)

	restore = snapstate.MockRevisionDate(nil)
	defer restore()

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

	prevSI := snap.SideInfo{
		RealName: snapName,
		Revision: prevSnapRev,
		SnapID:   snapID,
		Channel:  channel,
	}
	otherSI := snap.SideInfo{
		RealName: snapName,
		Revision: snap.R(99),
		SnapID:   snapID,
		Channel:  channel,
	}

	seq := snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&otherSI, &prevSI, &currentSI})

	currentKmodComps := make([]*snap.ComponentSideInfo, 0, len(components))
	prevKmodComps := make([]*snap.ComponentSideInfo, 0, len(components))
	currentResources := make(map[string]snap.Revision, len(components))

	for i, comp := range components {
		desiredCsi := snap.ComponentSideInfo{
			Component: naming.NewComponentRef(snapName, comp),
			Revision:  snap.R(i + 2),
		}
		err := seq.AddComponentForRevision(prevSnapRev, &sequence.ComponentState{
			SideInfo: &desiredCsi,
			CompType: componentNameToType(c, comp),
		})
		c.Assert(err, IsNil)

		currentCsi := snap.ComponentSideInfo{
			Component: naming.NewComponentRef(snapName, comp),
			Revision:  snap.R(i + 4),
		}
		err = seq.AddComponentForRevision(currentSnapRev, &sequence.ComponentState{
			SideInfo: &currentCsi,
			CompType: componentNameToType(c, comp),
		})
		c.Assert(err, IsNil)

		if strings.HasPrefix(comp, string(snap.KernelModulesComponent)) {
			prevKmodComps = append(prevKmodComps, &desiredCsi)
			currentKmodComps = append(currentKmodComps, &currentCsi)
		}
		currentResources[comp] = currentCsi.Revision
	}

	s.fakeStore.snapResourcesFn = func(info *snap.Info) []store.SnapResourceResult {
		c.Assert(info.InstanceName(), DeepEquals, snapName)
		var results []store.SnapResourceResult
		for i, compName := range components {
			results = append(results, store.SnapResourceResult{
				DownloadInfo: snap.DownloadInfo{DownloadURL: "http://example.com/" + compName},
				Name:         compName,
				Revision:     i + 2,
				Type:         fmt.Sprintf("component/%s", componentNameToType(c, compName)),
				Version:      "1.0",
				CreatedAt:    "2024-01-01T00:00:00Z",
			})
		}
		return results
	}

	s.AddCleanup(snapstate.MockReadComponentInfo(func(
		compMntDir string, info *snap.Info, csi *snap.ComponentSideInfo,
	) (*snap.ComponentInfo, error) {
		return &snap.ComponentInfo{
			Component:         csi.Component,
			Type:              componentNameToType(c, csi.Component.ComponentName),
			CompVersion:       "1.0",
			ComponentSideInfo: *csi,
		}, nil
	}))

	snapstate.Set(s.state, snapName, &snapstate.SnapState{
		Active:          true,
		Sequence:        seq,
		Current:         currentSI.Revision,
		SnapType:        "kernel",
		TrackingChannel: channel,
	})

	ts, err := snapstate.UpdateOne(context.Background(), s.state, snapstate.StoreUpdateGoal(snapstate.StoreUpdate{
		InstanceName: snapName,
		RevOpts: snapstate.RevisionOptions{
			Revision: prevSnapRev,
		},
	}), nil, snapstate.Options{
		UserID: s.user.ID,
		Flags: snapstate.Flags{
			Transaction: client.TransactionPerSnap,
		},
	})
	c.Assert(err, IsNil)

	var linkTasks int
	for _, t := range ts.Tasks() {
		if t.Kind() == "link-component" {
			linkTasks++
		}
	}
	c.Check(linkTasks, Equals, 0)

	chg := s.state.NewChange("refresh", "refresh a snap")
	chg.AddAll(ts)

	if undo {
		last := lastWithLane(ts.Tasks())
		c.Assert(last, NotNil)

		terr := s.state.NewTask("error-trigger", "provoking total undo")
		terr.WaitFor(last)
		terr.JoinLane(last.Lanes()[0])
		chg.AddTask(terr)
	}

	te := ts.MaybeEdge(snapstate.LastBeforeLocalModificationsEdge)
	c.Assert(te, NotNil)
	c.Assert(te.Kind(), Equals, "validate-component")

	s.settle(c)

	if undo {
		c.Assert(chg.Err(), NotNil, Commentf("change tasks:\n%s", printTasks(chg.Tasks())))
	} else {
		c.Assert(chg.Err(), IsNil, Commentf("change tasks:\n%s", printTasks(chg.Tasks())))
	}

	c.Check(s.fakeBackend.ops.Count("link-component"), Equals, 0)
	c.Check(s.fakeBackend.ops.Count("unlink-component"), Equals, 0)

	var kmodOps fakeOps
	for _, op := range s.fakeBackend.ops {
		if op.op == "prepare-kernel-modules-components" {
			kmodOps = append(kmodOps, op)
		}
	}

	if undo {
		c.Assert(kmodOps, HasLen, 2)
		c.Check(kmodOps[0].currentComps, DeepEquals, currentKmodComps)
		c.Check(kmodOps[0].finalComps, DeepEquals, prevKmodComps)
		c.Check(kmodOps[1].currentComps, DeepEquals, prevKmodComps)
		c.Check(kmodOps[1].finalComps, DeepEquals, currentKmodComps)
	} else {
		c.Assert(kmodOps, HasLen, 1)
		c.Check(kmodOps[0].currentComps, DeepEquals, currentKmodComps)
		c.Check(kmodOps[0].finalComps, DeepEquals, prevKmodComps)
	}

	task := ts.Tasks()[1]
	var snapsup snapstate.SnapSetup
	err = task.Get("snap-setup", &snapsup)
	c.Assert(err, IsNil)
	c.Check(snapsup.PreUpdateKernelModuleComponents, DeepEquals, currentKmodComps)

	var snapst snapstate.SnapState
	err = snapstate.Get(s.state, snapName, &snapst)
	c.Assert(err, IsNil)

	if undo {
		c.Assert(snapst.Active, Equals, true)
		c.Assert(snapst.Current, Equals, currentSnapRev)
		c.Assert(snapst.Sequence, DeepEquals, seq)
		return
	}

	c.Assert(snapst.LastRefreshTime, NotNil)
	c.Assert(snapst.Active, Equals, true)
	c.Assert(snapst.Current, Equals, prevSnapRev)
	c.Assert(snapst.Sequence.Revisions, HasLen, 3)
	c.Assert(snapst.Sequence.Revisions[2], DeepEquals, seq.Revisions[1])
	c.Assert(snapst.Sequence.Revisions[1], DeepEquals, seq.Revisions[2])
	c.Assert(snapst.Sequence.Revisions[0], DeepEquals, seq.Revisions[0])
}

func (s *snapmgrTestSuite) TestUpdateWithComponentsBackToPrevRevisionAddComponents(c *C) {
	const (
		snapName = "kernel-snap-with-components"
		snapID   = "kernel-snap-with-components-id"
		channel  = "channel-for-components-only-component-refresh"
	)

	r := snapstatetest.MockDeviceModel(MakeModel20("pc", map[string]any{"base": "core24"}))
	defer r()

	currentSnapRev := snap.R(11)
	prevSnapRev := snap.R(7)

	// we start without the auxiliary store info (or with an older one)
	c.Check(backend.AuxStoreInfoFilename(snapID), testutil.FileAbsent)

	currentSI := snap.SideInfo{
		RealName: snapName,
		Revision: currentSnapRev,
		SnapID:   snapID,
		Channel:  channel,
	}

	snaptest.MockSnap(c, fmt.Sprintf("name: %s\ntype: kernel\n", snapName), &currentSI)

	restore := snapstate.MockRevisionDate(nil)
	defer restore()

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

	prevSI := snap.SideInfo{
		RealName: snapName,
		Revision: prevSnapRev,
		SnapID:   snapID,
		Channel:  channel,
	}

	otherSI := snap.SideInfo{
		RealName: snapName,
		Revision: snap.R(99),
		SnapID:   snapID,
		Channel:  channel,
	}

	seq := snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&otherSI, &prevSI, &currentSI})

	components := []string{"kernel-modules-component"}

	s.fakeStore.snapResourcesFn = func(info *snap.Info) []store.SnapResourceResult {
		c.Assert(info.InstanceName(), DeepEquals, snapName)
		var results []store.SnapResourceResult
		for i, compName := range components {
			results = append(results, store.SnapResourceResult{
				DownloadInfo: snap.DownloadInfo{
					DownloadURL: "http://example.com/" + compName,
				},
				Name:      compName,
				Revision:  i + 2,
				Type:      fmt.Sprintf("component/%s", componentNameToType(c, compName)),
				Version:   "1.0",
				CreatedAt: "2024-01-01T00:00:00Z",
			})
		}
		return results
	}

	s.AddCleanup(snapstate.MockReadComponentInfo(func(
		compMntDir string, info *snap.Info, csi *snap.ComponentSideInfo,
	) (*snap.ComponentInfo, error) {
		return &snap.ComponentInfo{
			Component:         csi.Component,
			Type:              componentNameToType(c, csi.Component.ComponentName),
			CompVersion:       "1.0",
			ComponentSideInfo: *csi,
		}, nil
	}))

	snapstate.Set(s.state, snapName, &snapstate.SnapState{
		Active:          true,
		Sequence:        seq,
		Current:         currentSI.Revision,
		SnapType:        "kernel",
		TrackingChannel: channel,
	})

	ts, err := snapstate.UpdateOne(context.Background(), s.state, snapstate.StoreUpdateGoal(snapstate.StoreUpdate{
		InstanceName:         snapName,
		RevOpts:              snapstate.RevisionOptions{Revision: prevSnapRev},
		AdditionalComponents: components,
	}), nil, snapstate.Options{
		UserID: s.user.ID,
		Flags: snapstate.Flags{
			Transaction: client.TransactionPerSnap,
		},
	})
	c.Assert(err, IsNil)

	chg := s.state.NewChange("refresh", "refresh a snap")
	chg.AddAll(ts)

	// check unlink-reason
	unlinkTask := findLastTask(chg, "unlink-current-snap")
	c.Assert(unlinkTask, NotNil)
	var unlinkReason string
	unlinkTask.Get("unlink-reason", &unlinkReason)
	c.Check(unlinkReason, Equals, "refresh")

	// local modifications, edge must be set
	te := ts.MaybeEdge(snapstate.LastBeforeLocalModificationsEdge)
	c.Assert(te, NotNil)
	c.Assert(te.Kind(), Equals, "validate-component")

	s.settle(c)

	c.Assert(chg.Err(), IsNil, Commentf("change tasks:\n%s", printTasks(chg.Tasks())))

	fi, err := os.Stat(snap.MountFile(snapName, currentSnapRev))
	c.Assert(err, IsNil)

	expected := fakeOps{
		{
			op: "storesvc-snap-action",
			curSnaps: []store.CurrentSnap{{
				InstanceName:    snapName,
				SnapID:          snapID,
				Revision:        currentSnapRev,
				TrackingChannel: channel,
				RefreshedDate:   fi.ModTime(),
				Epoch:           snap.E("1*"),
				Resources:       map[string]snap.Revision{},
			}},
			userID: 1,
		},
		{
			op: "storesvc-snap-action:action",
			action: store.SnapAction{
				Action:          "refresh",
				InstanceName:    snapName,
				Revision:        prevSnapRev,
				SnapID:          snapID,
				ResourceInstall: true,
				// note that channel is empty here since we can't provide a
				// channel in this case, since we don't know if the revision is
				// a part of the channel
				Channel: "",
			},
			revno:  prevSnapRev,
			userID: 1,
		},
	}

	for i, compName := range components {
		csi := snap.ComponentSideInfo{
			Component: naming.NewComponentRef(snapName, compName),
			Revision:  snap.R(i + 2),
		}

		filename := fmt.Sprintf("%s_%v.comp", csi.Component, csi.Revision)

		expected = append(expected, []fakeOp{{
			op:   "storesvc-download",
			name: csi.Component.String(),
		}, {
			op:                "validate-component:Doing",
			name:              snapName,
			revno:             prevSnapRev,
			componentName:     compName,
			componentPath:     filepath.Join(dirs.SnapBlobDir, filename),
			componentRev:      csi.Revision,
			componentSideInfo: csi,
		}, {
			op:                "setup-component",
			containerName:     csi.Component.String(),
			containerFileName: filename,
		}}...)
	}

	expected = append(expected, fakeOp{
		op:   "remove-snap-aliases",
		name: snapName,
	})

	expected = append(expected, fakeOps{
		{
			op:          "run-inhibit-snap-for-unlink",
			name:        snapName,
			inhibitHint: "refresh",
		},
		{
			op:   "discard-namespace-locked",
			name: snapName,
		},
		{
			op:          "unlink-snap",
			path:        filepath.Join(dirs.SnapMountDir, snapName, currentSnapRev.String()),
			inhibitHint: "refresh",
		},
		{
			op: "prepare-kernel-snap",
		},
		{
			op:    "update-gadget-assets:Doing",
			name:  "kernel-snap-with-components",
			revno: prevSnapRev,
		},
		{
			op:   "copy-data",
			path: filepath.Join(dirs.SnapMountDir, snapName, prevSnapRev.String()),
			old:  filepath.Join(dirs.SnapMountDir, snapName, currentSnapRev.String()),
		},
		{
			op:   "setup-snap-save-data",
			path: filepath.Join(dirs.SnapDataSaveDir, snapName),
		},
		{
			op:    "setup-profiles:Doing",
			name:  snapName,
			revno: prevSnapRev,
		},
		{
			op: "candidate",
			sinfo: snap.SideInfo{
				RealName: snapName,
				SnapID:   snapID,
				Channel:  channel,
				Revision: prevSnapRev,
			},
		},
		{
			op:                  "link-snap",
			path:                filepath.Join(dirs.SnapMountDir, snapName, prevSnapRev.String()),
			requireSnapdTooling: true,
		},
		{
			op:   "link-component",
			path: snap.ComponentMountDir("kernel-modules-component", snap.R(2), snapName),
		},
	}...)

	newKmodComps := []*snap.ComponentSideInfo{{
		Component: naming.NewComponentRef(snapName, "kernel-modules-component"),
		Revision:  snap.R(2),
	}}

	expected = append(expected, fakeOps{
		{
			op:    "auto-connect:Doing",
			name:  snapName,
			revno: prevSnapRev,
		},
		{
			op: "update-aliases",
		},
		{
			op:           "prepare-kernel-modules-components",
			currentComps: []*snap.ComponentSideInfo{},
			finalComps:   newKmodComps,
		},
		{
			op: "maybe-set-next-boot",
		},
		{
			op: "remove-kernel-snap-setup",
		},
	}...)

	expected = append(expected, fakeOp{
		op:    "cleanup-trash",
		name:  snapName,
		revno: prevSnapRev,
	})

	// start with an easier-to-read error if this fails:
	c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
	c.Assert(s.fakeBackend.ops, DeepEquals, expected)

	task := ts.Tasks()[1]

	// verify snapSetup info
	var snapsup snapstate.SnapSetup
	err = task.Get("snap-setup", &snapsup)
	c.Assert(err, IsNil)
	c.Assert(snapsup, DeepEquals, snapstate.SnapSetup{
		Channel: channel,
		UserID:  s.user.ID,

		SnapPath:  filepath.Join(dirs.SnapBlobDir, fmt.Sprintf("%s_%v.snap", snapName, prevSnapRev)),
		SideInfo:  snapsup.SideInfo,
		Type:      snap.TypeKernel,
		Version:   "kernel-snap-with-componentsVer",
		PlugsOnly: true,
		Flags: snapstate.Flags{
			Transaction: client.TransactionPerSnap,
		},
		PreUpdateKernelModuleComponents: []*snap.ComponentSideInfo{},
	})
	c.Assert(snapsup.SideInfo, DeepEquals, &snap.SideInfo{
		RealName: snapName,
		Revision: prevSnapRev,
		Channel:  channel,
		SnapID:   snapID,
	})

	// verify snaps in the system state
	var snapst snapstate.SnapState
	err = snapstate.Get(s.state, snapName, &snapst)
	c.Assert(err, IsNil)

	c.Assert(snapst.LastRefreshTime, NotNil)
	c.Assert(snapst.Active, Equals, true)
	c.Assert(snapst.Sequence.Revisions, HasLen, 3)

	// the original revision's components should be replaced with the new
	// components
	seq.Revisions[1].Components = nil
	for i, comp := range components {
		err := seq.AddComponentForRevision(prevSnapRev, &sequence.ComponentState{
			SideInfo: &snap.ComponentSideInfo{
				Component: naming.NewComponentRef(snapName, comp),
				Revision:  snap.R(i + 2),
			},
			CompType: componentNameToType(c, comp),
		})
		c.Assert(err, IsNil)
	}

	// link-snap should put the revision we refreshed to at the end of the
	// sequence
	c.Assert(snapst.Sequence.Revisions[2], DeepEquals, seq.Revisions[1])
	c.Assert(snapst.Sequence.Revisions[1], DeepEquals, seq.Revisions[2])
	c.Assert(snapst.Sequence.Revisions[0], DeepEquals, seq.Revisions[0])
}

func (s *snapmgrTestSuite) TestUpdateWithComponentsRunThrough(c *C) {
	s.testUpdateWithComponentsRunThrough(c, updateWithComponentsOpts{
		snapType:   snap.TypeKernel,
		components: []string{"standard-component", "kernel-modules-component"},
	})
}

func (s *snapmgrTestSuite) TestUpdateWithComponentsRunThroughNoComponents(c *C) {
	s.testUpdateWithComponentsRunThrough(c, updateWithComponentsOpts{snapType: snap.TypeKernel})
}

func (s *snapmgrTestSuite) TestUpdateExplicitlyToSameRevisionRunThrough(c *C) {
	s.testUpdateWithComponentsRunThrough(c, updateWithComponentsOpts{
		snapType:              snap.TypeKernel,
		useSameSnapRev:        true,
		refreshAppAwarenessUX: true,
	})
}

func (s *snapmgrTestSuite) TestUpdateExplicitlyToSameRevisionRunThroughUndo(c *C) {
	s.testUpdateWithComponentsRunThrough(c, updateWithComponentsOpts{
		snapType:              snap.TypeKernel,
		useSameSnapRev:        true,
		undo:                  true,
		refreshAppAwarenessUX: true,
	})
}

func (s *snapmgrTestSuite) TestUpdateWithComponentsRunThroughUndo(c *C) {
	s.testUpdateWithComponentsRunThrough(c, updateWithComponentsOpts{
		snapType:              snap.TypeKernel,
		components:            []string{"standard-component", "kernel-modules-component"},
		refreshAppAwarenessUX: true,
		undo:                  true,
	})
}

func (s *snapmgrTestSuite) TestUpdateWithComponentsRunThroughInstanceKey(c *C) {
	s.testUpdateWithComponentsRunThrough(c, updateWithComponentsOpts{
		snapType:              snap.TypeApp,
		instanceKey:           "key",
		components:            []string{"standard-component", "standard-component-extra"},
		refreshAppAwarenessUX: true,
		undo:                  false,
	})
}

func (s *snapmgrTestSuite) TestUpdateWithComponentsRunThroughInstanceKeyUndo(c *C) {
	s.testUpdateWithComponentsRunThrough(c, updateWithComponentsOpts{
		snapType:              snap.TypeApp,
		instanceKey:           "key",
		components:            []string{"standard-component", "standard-component-extra"},
		refreshAppAwarenessUX: true,
		undo:                  true,
	})
}

func (s *snapmgrTestSuite) TestUpdateWithComponentsRunThroughLoseComponents(c *C) {
	s.testUpdateWithComponentsRunThrough(c, updateWithComponentsOpts{
		snapType:              snap.TypeKernel,
		components:            []string{"standard-component", "kernel-modules-component"},
		postRefreshComponents: []string{"standard-component"},
		refreshAppAwarenessUX: true,
	})
}

func (s *snapmgrTestSuite) TestUpdateWithComponentsRunThroughLoseComponentsUndo(c *C) {
	s.testUpdateWithComponentsRunThrough(c, updateWithComponentsOpts{
		snapType:              snap.TypeKernel,
		components:            []string{"standard-component", "kernel-modules-component"},
		postRefreshComponents: []string{"standard-component"},
		refreshAppAwarenessUX: true,
		undo:                  true,
	})
}

func (s *snapmgrTestSuite) TestUpdateWithComponentsRunThroughAdditionalComponents(c *C) {
	s.testUpdateWithComponentsRunThrough(c, updateWithComponentsOpts{
		snapType:              snap.TypeKernel,
		components:            []string{"standard-component"},
		postRefreshComponents: []string{"standard-component", "kernel-modules-component"},
		additionalComponents:  []string{"kernel-modules-component"},
		refreshAppAwarenessUX: true,
	})
}

func (s *snapmgrTestSuite) TestUpdateWithComponentsRunThroughAdditionalComponentsAlreadyInstalled(c *C) {
	s.testUpdateWithComponentsRunThrough(c, updateWithComponentsOpts{
		snapType:              snap.TypeKernel,
		components:            []string{"standard-component"},
		postRefreshComponents: []string{"standard-component"},
		additionalComponents:  []string{"standard-component"},
		refreshAppAwarenessUX: true,
	})
}

func (s *snapmgrTestSuite) TestUpdateWithComponentsRunThroughAdditionalComponentsUndo(c *C) {
	s.testUpdateWithComponentsRunThrough(c, updateWithComponentsOpts{
		snapType:              snap.TypeKernel,
		components:            []string{"standard-component"},
		postRefreshComponents: []string{"standard-component", "kernel-modules-component"},
		additionalComponents:  []string{"kernel-modules-component"},
		refreshAppAwarenessUX: true,
		undo:                  true,
	})
}

type updateWithComponentsOpts struct {
	instanceKey           string
	snapType              snap.Type
	components            []string
	postRefreshComponents []string
	additionalComponents  []string
	refreshAppAwarenessUX bool
	undo                  bool
	useSameSnapRev        bool
}

func findKindInTaskSet(ts *state.TaskSet, kind string) *state.Task {
	for _, t := range ts.Tasks() {
		if t.Kind() == kind {
			return t
		}
	}
	return nil
}

func componentNameToType(c *C, name string) snap.ComponentType {
	typ, _, ok := strings.Cut(name, "-component")
	if !ok {
		c.Fatalf("unexpected component name %q", name)
	}
	return snap.ComponentType(typ)
}

func (s *snapmgrTestSuite) testUpdateWithComponentsRunThrough(c *C, opts updateWithComponentsOpts) {
	if opts.refreshAppAwarenessUX {
		s.enableRefreshAppAwarenessUX()
	}

	var snapName, snapID string
	switch opts.snapType {
	case snap.TypeKernel:
		s.fakeBackend.linkSnapMaybeReboot = true
		snapName = "kernel-snap-with-components"
		snapID = "kernel-snap-with-components-id"
	case snap.TypeApp:
		snapName = "app-snap-with-components"
		snapID = "app-snap-with-components-id"
	default:
		c.Error("boo")
	}

	r := snapstatetest.MockDeviceModel(MakeModel20("pc", map[string]any{"base": "core24"}))
	defer r()

	channel := "channel-for-components"
	currentSnapRev := snap.R(7)

	newSnapRev := snap.R(11)
	if opts.useSameSnapRev {
		newSnapRev = currentSnapRev
	}

	instanceName := snap.InstanceName(snapName, opts.instanceKey)

	if opts.postRefreshComponents == nil {
		opts.postRefreshComponents = opts.components
	}

	sort.Strings(opts.components)
	sort.Strings(opts.postRefreshComponents)

	originalCompRevisions := make(map[string]snap.Revision)
	for i, compName := range opts.components {
		originalCompRevisions[compName] = snap.R(i + 1)
	}

	updatedCompRevisions := make(map[string]snap.Revision)
	for _, compName := range opts.postRefreshComponents {
		updatedCompRevisions[compName] = snap.R(originalCompRevisions[compName].N + 1)
	}

	s.fakeStore.snapResourcesFn = func(info *snap.Info) []store.SnapResourceResult {
		c.Assert(info.InstanceName(), DeepEquals, instanceName)
		var results []store.SnapResourceResult
		for _, compName := range opts.postRefreshComponents {
			results = append(results, store.SnapResourceResult{
				DownloadInfo: snap.DownloadInfo{
					DownloadURL: "http://example.com/" + compName,
				},
				Name:      compName,
				Revision:  updatedCompRevisions[compName].N,
				Type:      fmt.Sprintf("component/%s", componentNameToType(c, compName)),
				Version:   "1.0",
				CreatedAt: "2024-01-01T00:00:00Z",
			})
		}
		return results
	}

	// we start without the auxiliary store info (or with an older one)
	c.Check(backend.AuxStoreInfoFilename(snapID), testutil.FileAbsent)

	si := snap.SideInfo{
		RealName: snapName,
		Revision: currentSnapRev,
		SnapID:   snapID,
		Channel:  channel,
	}
	snaptest.MockSnapInstance(c, instanceName, fmt.Sprintf("name: %s\ntype: %s\n",
		snapName, opts.snapType), &si)
	fi, err := os.Stat(snap.MountFile(instanceName, si.Revision))
	c.Assert(err, IsNil)

	refreshedDate := fi.ModTime()

	restore := snapstate.MockRevisionDate(nil)
	defer restore()

	now, err := time.Parse(time.RFC3339, "2021-06-10T10:00:00Z")
	c.Assert(err, IsNil)

	restore = snapstate.MockTimeNow(func() time.Time {
		return now
	})
	defer restore()

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

	if opts.instanceKey != "" {
		tr := config.NewTransaction(s.state)
		tr.Set("core", "experimental.parallel-instances", true)
		tr.Commit()
	}

	currentSeq := snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si})

	currentResources := make(map[string]snap.Revision, len(opts.components))
	for _, comp := range opts.components {
		err := currentSeq.AddComponentForRevision(currentSnapRev, &sequence.ComponentState{
			SideInfo: &snap.ComponentSideInfo{
				Component: naming.NewComponentRef(snapName, comp),
				Revision:  originalCompRevisions[comp],
			},
			CompType: componentNameToType(c, comp),
		})
		c.Assert(err, IsNil)

		if currentResources == nil {
			currentResources = make(map[string]snap.Revision, len(opts.components))
		}
		currentResources[comp] = originalCompRevisions[comp]
	}
	currentComponentStates := currentSeq.Revisions[0].Components

	var expectedComponentStates []*sequence.ComponentState
	for _, comp := range opts.postRefreshComponents {
		expectedComponentStates = append(expectedComponentStates, &sequence.ComponentState{
			SideInfo: &snap.ComponentSideInfo{
				Component: naming.NewComponentRef(snapName, comp),
				Revision:  updatedCompRevisions[comp],
			},
			CompType: componentNameToType(c, comp),
		})
	}

	newKmodComps := make([]*snap.ComponentSideInfo, 0, len(expectedComponentStates))
	for _, cs := range expectedComponentStates {
		if cs.CompType == snap.KernelModulesComponent {
			newKmodComps = append(newKmodComps, cs.SideInfo)
		}
	}

	currentKmodComps := make([]*snap.ComponentSideInfo, 0, len(currentComponentStates))
	for _, cs := range currentComponentStates {
		if cs.CompType == snap.KernelModulesComponent {
			currentKmodComps = append(currentKmodComps, cs.SideInfo)
		}
	}

	withKMods := len(currentKmodComps) > 0 || len(newKmodComps) > 0

	s.AddCleanup(snapstate.MockReadComponentInfo(func(
		compMntDir string, info *snap.Info, csi *snap.ComponentSideInfo,
	) (*snap.ComponentInfo, error) {
		return &snap.ComponentInfo{
			Component:         csi.Component,
			Type:              componentNameToType(c, csi.Component.ComponentName),
			CompVersion:       "1.0",
			ComponentSideInfo: *csi,
		}, nil
	}))

	snapstate.Set(s.state, instanceName, &snapstate.SnapState{
		Active:          true,
		Sequence:        currentSeq,
		Current:         si.Revision,
		SnapType:        string(opts.snapType),
		TrackingChannel: channel,
		InstanceKey:     opts.instanceKey,
	})

	var revOpts snapstate.RevisionOptions
	if opts.useSameSnapRev {
		revOpts.Revision = currentSnapRev
	}

	ts, err := snapstate.UpdateOne(context.Background(), s.state, snapstate.StoreUpdateGoal(snapstate.StoreUpdate{
		InstanceName:         instanceName,
		RevOpts:              revOpts,
		AdditionalComponents: opts.additionalComponents,
	}), nil, snapstate.Options{
		Flags: snapstate.Flags{
			NoReRefresh: true,
			Transaction: client.TransactionPerSnap,
		},
		UserID: s.user.ID,
	})
	c.Assert(err, IsNil)

	// Check boundaries are in the expected tasks
	var setNextBoot, finishRestart bool
	preRebootTask, err := ts.Edge(snapstate.MaybeRebootEdge)
	c.Assert(err, IsNil)
	postRebootTask, err := ts.Edge(snapstate.MaybeRebootWaitEdge)
	c.Assert(err, IsNil)
	if opts.snapType == snap.TypeKernel {
		var restartBoundary restart.RestartBoundaryDirection
		c.Check(preRebootTask.Get("restart-boundary", &restartBoundary), IsNil)
		c.Check(restartBoundary, Equals, restart.RestartBoundaryDirectionDo)
	}
	if withKMods {
		// Implies this is a kernel
		c.Check(preRebootTask.Kind(), Equals, "prepare-kernel-modules-components")
		c.Check(postRebootTask.Kind(), Equals, "discard-old-kernel-snap-setup")
		lnTask := findKindInTaskSet(ts, "link-snap")
		autoConnTask := findKindInTaskSet(ts, "auto-connect")
		c.Check(lnTask.Get("set-next-boot", &setNextBoot), IsNil)
		c.Check(setNextBoot, Equals, false)
		c.Check(autoConnTask.Get("finish-restart", &finishRestart), IsNil)
		c.Check(finishRestart, Equals, false)
	} else {
		c.Check(preRebootTask.Kind(), Equals, "link-snap")
		// snapd refreshes validate resealing first after the daemon restart.
		// Otherwise, if non-kernel module components are present, component
		// linking is the first post-reboot task. If not, it is auto-connect.
		if opts.snapType == snap.TypeSnapd {
			c.Check(postRebootTask.Kind(), Equals, "check-reseal")
		} else if len(opts.postRefreshComponents) > 0 {
			c.Check(postRebootTask.Kind(), Equals, "link-component")
		} else {
			c.Check(postRebootTask.Kind(), Equals, "auto-connect")
		}
		c.Check(findKindInTaskSet(ts, "prepare-kernel-modules-components"), IsNil)
		if opts.snapType == snap.TypeKernel {
			discardTask := findKindInTaskSet(ts, "discard-old-kernel-snap-setup")
			c.Check(discardTask.Get("finish-restart", &setNextBoot), IsNil)
			c.Check(setNextBoot, Equals, false)
		}
	}
	c.Check(preRebootTask.Get("set-next-boot", &setNextBoot), IsNil)
	c.Check(setNextBoot, Equals, true)

	// the first post-reboot task will always have finish-restart when there are
	// kernel module components present.
	//
	// however, when there are components present that are not kernel module
	// components, the auto-connect task will have finish-restart, despite it
	// not being the actual first task after the reboot. Snapd refreshes add an
	// explicit reseal validation task as the first post-reboot step, and keep
	// auto-connect marked for compatibility with the current behavior.
	if withKMods {
		c.Check(postRebootTask.Get("finish-restart", &finishRestart), IsNil)
		c.Check(finishRestart, Equals, true)
	} else {
		autoConnTask := findKindInTaskSet(ts, "auto-connect")
		c.Assert(autoConnTask, NotNil)
		c.Check(autoConnTask.Get("finish-restart", &finishRestart), IsNil)
		c.Check(finishRestart, Equals, true)
		if opts.snapType == snap.TypeSnapd {
			c.Check(postRebootTask.Get("finish-restart", &finishRestart), IsNil)
			c.Check(finishRestart, Equals, true)
		}
	}

	chg := s.state.NewChange("refresh", "refresh a snap")
	chg.AddAll(ts)

	if opts.undo {
		last := lastWithLane(ts.Tasks())
		c.Assert(last, NotNil)

		terr := s.state.NewTask("error-trigger", "provoking total undo")
		terr.WaitFor(last)
		terr.JoinLane(last.Lanes()[0])
		chg.AddTask(terr)
	}

	// check unlink-reason
	unlinkTask := findLastTask(chg, "unlink-current-snap")
	c.Assert(unlinkTask, NotNil)
	var unlinkReason string
	unlinkTask.Get("unlink-reason", &unlinkReason)
	c.Check(unlinkReason, Equals, "refresh")

	// local modifications, edge must be set
	te := ts.MaybeEdge(snapstate.LastBeforeLocalModificationsEdge)
	c.Assert(te, NotNil)

	if len(opts.components) == 0 {
		if opts.useSameSnapRev {
			c.Assert(te.Kind(), Equals, "prepare-snap")
		} else {
			c.Assert(te.Kind(), Equals, "validate-snap")
		}
	} else {
		if opts.useSameSnapRev {
			c.Assert(te.Kind(), Equals, "validate-component")
		} else {
			c.Assert(te.Kind(), Equals, "validate-component")
		}

	}

	// we manually settle here since this test can be slow when the host is
	// under load
	s.state.Unlock()
	err = s.o.Settle(testutil.HostScaledTimeout(15 * time.Second))
	s.state.Lock()
	c.Assert(err, IsNil, Commentf("change tasks:\n%s", printTasks(chg.Tasks())))

	var expected fakeOps
	if !opts.useSameSnapRev {
		expected = fakeOps{
			{
				op: "storesvc-snap-action",
				curSnaps: []store.CurrentSnap{{
					InstanceName:    instanceName,
					SnapID:          snapID,
					Revision:        currentSnapRev,
					TrackingChannel: channel,
					RefreshedDate:   refreshedDate,
					Epoch:           snap.E("1*"),
					Resources:       currentResources,
				}},
				userID: 1,
			},
			{
				op: "storesvc-snap-action:action",
				action: store.SnapAction{
					Action:       "refresh",
					InstanceName: instanceName,
					SnapID:       snapID,
					Channel:      channel,
					Flags:        store.SnapActionEnforceValidation,
				},
				revno:  newSnapRev,
				userID: 1,
			},
			{
				op:   "storesvc-download",
				name: snapName,
			},
			{
				op:    "validate-snap:Doing",
				name:  instanceName,
				revno: newSnapRev,
			},
		}
		for _, cs := range expectedComponentStates {
			compName := cs.SideInfo.Component.ComponentName
			compRev := cs.SideInfo.Revision
			containerName := fmt.Sprintf("%s+%s", instanceName, compName)
			filename := fmt.Sprintf("%s_%v.comp", containerName, compRev)

			expected = append(expected, []fakeOp{{
				op:   "storesvc-download",
				name: cs.SideInfo.Component.String(),
			}, {
				op:                "validate-component:Doing",
				name:              instanceName,
				revno:             newSnapRev,
				componentName:     compName,
				componentPath:     filepath.Join(dirs.SnapBlobDir, filename),
				componentRev:      compRev,
				componentSideInfo: *cs.SideInfo,
			}}...)
		}

		expected = append(expected, []fakeOp{
			{
				op:  "current",
				old: filepath.Join(dirs.SnapMountDir, instanceName, currentSnapRev.String()),
			},
			{
				op:   "open-snap-file",
				path: filepath.Join(dirs.SnapBlobDir, fmt.Sprintf("%s_%v.snap", instanceName, newSnapRev)),
				sinfo: snap.SideInfo{
					RealName: snapName,
					SnapID:   snapID,
					Channel:  channel,
					Revision: newSnapRev,
				},
			},
			{
				op:    "setup-snap",
				name:  instanceName,
				path:  filepath.Join(dirs.SnapBlobDir, fmt.Sprintf("%s_%v.snap", instanceName, newSnapRev)),
				revno: newSnapRev,
			},
		}...)
	} else {
		for _, cs := range expectedComponentStates {
			compName := cs.SideInfo.Component.ComponentName
			compRev := cs.SideInfo.Revision
			containerName := fmt.Sprintf("%s+%s", instanceName, compName)
			filename := fmt.Sprintf("%s_%v.comp", containerName, compRev)

			expected = append(expected, []fakeOp{{
				op:   "storesvc-download",
				name: cs.SideInfo.Component.String(),
			}, {
				op:                "validate-component:Doing",
				name:              instanceName,
				revno:             newSnapRev,
				componentName:     compName,
				componentPath:     filepath.Join(dirs.SnapBlobDir, filename),
				componentRev:      compRev,
				componentSideInfo: *cs.SideInfo,
			}}...)
		}
	}

	for _, cs := range expectedComponentStates {
		compName := cs.SideInfo.Component.ComponentName
		compRev := cs.SideInfo.Revision
		containerName := fmt.Sprintf("%s+%s", instanceName, compName)
		filename := fmt.Sprintf("%s_%v.comp", containerName, compRev)

		expected = append(expected, fakeOp{
			op:                "setup-component",
			containerName:     containerName,
			containerFileName: filename,
		})
	}

	if !opts.refreshAppAwarenessUX {
		expected = append(expected, fakeOp{
			op:   "remove-snap-aliases",
			name: instanceName,
		})
	}

	expected = append(expected, fakeOps{
		{
			op:          "run-inhibit-snap-for-unlink",
			name:        instanceName,
			inhibitHint: "refresh",
		},
		{
			op:   "discard-namespace-locked",
			name: instanceName,
		},
		{
			op:                 "unlink-snap",
			path:               filepath.Join(dirs.SnapMountDir, instanceName, currentSnapRev.String()),
			unlinkSkipBinaries: opts.refreshAppAwarenessUX,
			inhibitHint:        "refresh",
		}}...)
	if opts.snapType == snap.TypeKernel {
		expected = append(expected, fakeOp{
			op: "prepare-kernel-snap",
		}, fakeOp{
			op:    "update-gadget-assets:Doing",
			name:  instanceName,
			revno: newSnapRev,
		})
	}
	expected = append(expected, fakeOps{
		{
			op:   "copy-data",
			path: filepath.Join(dirs.SnapMountDir, instanceName, newSnapRev.String()),
			old:  filepath.Join(dirs.SnapMountDir, instanceName, currentSnapRev.String()),
		},
		{
			op:   "setup-snap-save-data",
			path: filepath.Join(dirs.SnapDataSaveDir, instanceName),
		},
	}...)

	expected = append(expected, fakeOps{
		{
			op:    "setup-profiles:Doing",
			name:  instanceName,
			revno: newSnapRev,
		},
		{
			op: "candidate",
			sinfo: snap.SideInfo{
				RealName: snapName,
				SnapID:   snapID,
				Channel:  channel,
				Revision: newSnapRev,
			},
		},
		{
			op:                  "link-snap",
			path:                filepath.Join(dirs.SnapMountDir, instanceName, newSnapRev.String()),
			requireSnapdTooling: true,
		},
	}...)

	if !withKMods {
		expected = append(expected, fakeOp{
			op: "maybe-set-next-boot",
		})
		if s.fakeBackend.linkSnapMaybeReboot {
			// check for tasks up to rebooting
			// start with an easier-to-read error if this fails:
			c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
			c.Assert(s.fakeBackend.ops, DeepEquals, expected)
			// mock restart step and run change to completion.
			s.mockRestartAndSettle(c, chg)
		}
	}

	for _, cs := range expectedComponentStates {
		compName := cs.SideInfo.Component.ComponentName
		compRev := cs.SideInfo.Revision
		expected = append(expected, fakeOp{
			op:   "link-component",
			path: snap.ComponentMountDir(compName, compRev, instanceName),
		})
	}

	expected = append(expected, fakeOps{
		{
			op:    "auto-connect:Doing",
			name:  instanceName,
			revno: newSnapRev,
		},
		{
			op: "update-aliases",
		},
	}...)

	if withKMods {
		expected = append(expected, fakeOp{
			op:           "prepare-kernel-modules-components",
			currentComps: currentKmodComps,
			finalComps:   newKmodComps,
		}, fakeOp{
			op: "maybe-set-next-boot",
		})
		// check for tasks up to rebooting
		// start with an easier-to-read error if this fails:
		c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
		c.Assert(s.fakeBackend.ops, DeepEquals, expected)
		// mock restart step and run change to completion.
		s.mockRestartAndSettle(c, chg)
	}

	// We've now run the task to completion also for reboot cases
	if opts.undo {
		c.Assert(chg.Err(), NotNil, Commentf("change tasks:\n%s", printTasks(chg.Tasks())))
	} else {
		c.Assert(chg.Err(), IsNil, Commentf("change tasks:\n%s", printTasks(chg.Tasks())))
	}

	if opts.snapType == snap.TypeKernel {
		expected = append(expected, fakeOp{
			op: "remove-kernel-snap-setup",
		})
	}

	expectedSI := snap.SideInfo{
		RealName: snapName,
		Revision: newSnapRev,
		SnapID:   snapID,
		Channel:  channel,
	}
	expectedSideState := sequence.NewRevisionSideState(&expectedSI, expectedComponentStates)
	originalSideState := currentSeq.Revisions[0]

	if opts.undo {
		expected = append(expected,
			undoOps(instanceName, opts.snapType, expectedSideState, originalSideState)...)
		if !opts.useSameSnapRev {
			expected = append(expected, fakeOp{
				op:   "storesvc-cleanup-download-artifacts",
				sha3: "<some-hash>",
				path: snap.MountFile(instanceName, newSnapRev),
			})
		}
	} else {
		expected = append(expected, fakeOp{
			op:    "cleanup-trash",
			name:  instanceName,
			revno: newSnapRev,
		})
	}

	var downloads []fakeDownload
	if !opts.useSameSnapRev {
		downloads = []fakeDownload{{
			macaroon: s.user.StoreMacaroon,
			name:     snapName,
			target:   filepath.Join(dirs.SnapBlobDir, fmt.Sprintf("%s_%v.snap", instanceName, newSnapRev)),
		}}
	}
	for _, compName := range opts.postRefreshComponents {
		downloads = append(downloads, fakeDownload{
			macaroon: s.user.StoreMacaroon,
			name:     fmt.Sprintf("%s+%s", snapName, compName),
			target:   filepath.Join(dirs.SnapBlobDir, fmt.Sprintf("%s+%s_%d.comp", instanceName, compName, updatedCompRevisions[compName].N)),
		})
	}

	c.Check(s.fakeStore.downloads, DeepEquals, downloads)

	if !opts.useSameSnapRev {
		c.Check(s.fakeStore.seenPrivacyKeys["privacy-key"], Equals, true, Commentf("salts seen: %v", s.fakeStore.seenPrivacyKeys))
	}

	// start with an easier-to-read error if this fails:
	c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
	c.Assert(s.fakeBackend.ops, DeepEquals, expected)

	// check progress
	task := ts.Tasks()[1]

	if !opts.useSameSnapRev {
		_, cur, total := task.Progress()
		c.Assert(cur, Equals, s.fakeStore.fakeCurrentProgress)
		c.Assert(total, Equals, s.fakeStore.fakeTotalProgress)
	}

	// verify snapSetup info
	var snapsup snapstate.SnapSetup
	err = task.Get("snap-setup", &snapsup)
	c.Assert(err, IsNil)

	expectedSnapsup := snapstate.SnapSetup{
		Channel: channel,
		UserID:  s.user.ID,

		SideInfo:  snapsup.SideInfo,
		Type:      opts.snapType,
		Version:   snapName + "Ver",
		PlugsOnly: true,
		Flags: snapstate.Flags{
			Transaction: client.TransactionPerSnap,
		},
		InstanceKey:                     opts.instanceKey,
		PreUpdateKernelModuleComponents: currentKmodComps,
	}

	if !opts.useSameSnapRev {
		expectedSnapsup.DownloadInfo = &snap.DownloadInfo{
			DownloadURL: "https://some-server.com/some/path.snap",
			Sha3_384:    "<some-hash>",
		}
	} else {
		expectedSnapsup.SnapPath = filepath.Join(dirs.SnapBlobDir, fmt.Sprintf("%s_%v.snap", instanceName, newSnapRev))
	}

	c.Assert(snapsup, DeepEquals, expectedSnapsup)
	c.Assert(snapsup.SideInfo, DeepEquals, &snap.SideInfo{
		RealName: snapName,
		Revision: newSnapRev,
		Channel:  channel,
		SnapID:   snapID,
	})

	compsups := make([]snapstate.ComponentSetup, 0, len(expectedComponentStates))
	for _, cs := range expectedComponentStates {
		compName := cs.SideInfo.Component.ComponentName
		compsups = append(compsups, snapstate.ComponentSetup{
			CompSideInfo: cs.SideInfo,
			CompType:     cs.CompType,
			DownloadInfo: &snap.DownloadInfo{
				DownloadURL: "http://example.com/" + compName,
			},
			CompPath: filepath.Join(dirs.SnapBlobDir, fmt.Sprintf("%s+%s_%s.comp", instanceName, compName, cs.SideInfo.Revision)),
			ComponentInstallFlags: snapstate.ComponentInstallFlags{
				MultiComponentInstall: true,
			},
		})
	}
	checkComponentSetupTasks(c, ts, compsups, "download-component")

	// verify snaps in the system state
	var snapst snapstate.SnapState
	err = snapstate.Get(s.state, instanceName, &snapst)
	c.Assert(err, IsNil)

	if !opts.undo {
		c.Assert(snapst.LastRefreshTime, NotNil)
		c.Check(snapst.LastRefreshTime.Equal(now), Equals, true)
		c.Assert(snapst.Active, Equals, true)
		if opts.useSameSnapRev {
			c.Assert(snapst.Sequence.Revisions, HasLen, 1)
		} else {
			c.Assert(snapst.Sequence.Revisions, HasLen, 2)
		}
		c.Assert(snapst.Sequence.Revisions[0], DeepEquals, currentSeq.Revisions[0])

		cand := sequence.NewRevisionSideState(&snap.SideInfo{
			RealName: snapName,
			Channel:  channel,
			SnapID:   snapID,
			Revision: newSnapRev,
		}, expectedComponentStates)

		if opts.useSameSnapRev {
			currentSeq.Revisions = []*sequence.RevisionSideState{cand}
		} else {
			currentSeq.Revisions = append(currentSeq.Revisions, cand)
		}

		c.Assert(snapst.Sequence, DeepEquals, currentSeq)

		// we end up with the auxiliary store info
		c.Check(backend.AuxStoreInfoFilename(snapID), testutil.FilePresent)
	} else {
		// make sure everything is back to how it started
		c.Assert(snapst.Active, Equals, true)
		c.Assert(snapst.Sequence.Revisions, HasLen, 1)
		c.Assert(snapst.Sequence.Revisions[0], DeepEquals, currentSeq.Revisions[0])
	}
}

func (s *snapmgrTestSuite) TestUpdateWithComponentsRunThroughShareComponents(c *C) {
	const undo = false
	s.testUpdateWithComponentsRunThroughShareComponents(c, undo)
}

func (s *snapmgrTestSuite) TestUpdateWithComponentsRunThroughShareComponentsUndo(c *C) {
	const undo = true
	s.testUpdateWithComponentsRunThroughShareComponents(c, undo)
}

func (s *snapmgrTestSuite) testUpdateWithComponentsRunThroughShareComponents(c *C, undo bool) {
	s.enableRefreshAppAwarenessUX()

	const (
		snapName = "kernel"
		snapID   = "kernel-id"
	)

	restore := snapstatetest.MockDeviceModel(MakeModel20("pc", map[string]any{"base": "core24"}))
	defer restore()

	channel := "channel-for-components"
	currentSnapRev := snap.R(7)

	newSnapRev := snap.R(11)

	components := []string{"kernel-modules-component", "standard-component"}

	originalCompRevisions := make(map[string]snap.Revision)
	for i, compName := range components {
		originalCompRevisions[compName] = snap.R(i + 1)
	}

	s.fakeStore.snapResourcesFn = func(info *snap.Info) []store.SnapResourceResult {
		c.Assert(info.InstanceName(), DeepEquals, snapName)
		var results []store.SnapResourceResult
		for _, compName := range components {
			results = append(results, store.SnapResourceResult{
				DownloadInfo: snap.DownloadInfo{
					DownloadURL: "http://example.com/" + compName,
				},
				Name:      compName,
				Revision:  originalCompRevisions[compName].N,
				Type:      fmt.Sprintf("component/%s", componentNameToType(c, compName)),
				Version:   "1.0",
				CreatedAt: "2024-01-01T00:00:00Z",
			})
		}
		return results
	}

	c.Check(backend.AuxStoreInfoFilename(snapID), testutil.FileAbsent)

	si := snap.SideInfo{
		RealName: snapName,
		Revision: currentSnapRev,
		SnapID:   snapID,
		Channel:  channel,
	}
	snaptest.MockSnapInstance(c, snapName, fmt.Sprintf("name: %s\ntype: kernel", snapName), &si)
	fi, err := os.Stat(snap.MountFile(snapName, si.Revision))
	c.Assert(err, IsNil)

	bl := boottest.MockUC20RunBootenv(bootloadertest.Mock("mock", c.MkDir()))
	bl.SetBootVars(map[string]string{"snap_kernel": snapName, "snap_core": "core24_2.snap"})
	bootloader.Force(bl)
	defer bootloader.Force(nil)

	kPlaceInfo, err := snap.ParsePlaceInfoFromSnapFileName("kernel_" + newSnapRev.String() + ".snap")
	c.Assert(err, IsNil)
	restore = bl.SetEnabledKernel(kPlaceInfo)
	defer restore()

	refreshedDate := fi.ModTime()

	restore = snapstate.MockRevisionDate(nil)
	defer restore()

	now, err := time.Parse(time.RFC3339, "2021-06-10T10:00:00Z")
	c.Assert(err, IsNil)

	restore = snapstate.MockTimeNow(func() time.Time {
		return now
	})
	defer restore()

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

	currentSeq := snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si})

	currentResources := make(map[string]snap.Revision, len(components))
	for _, comp := range components {
		err := currentSeq.AddComponentForRevision(currentSnapRev, &sequence.ComponentState{
			SideInfo: &snap.ComponentSideInfo{
				Component: naming.NewComponentRef(snapName, comp),
				Revision:  originalCompRevisions[comp],
			},
			CompType: componentNameToType(c, comp),
		})
		c.Assert(err, IsNil)

		if currentResources == nil {
			currentResources = make(map[string]snap.Revision, len(components))
		}
		currentResources[comp] = originalCompRevisions[comp]
	}
	currentComponentStates := currentSeq.Revisions[0].Components
	expectedComponentStates := currentComponentStates

	s.AddCleanup(snapstate.MockReadComponentInfo(func(
		compMntDir string, info *snap.Info, csi *snap.ComponentSideInfo,
	) (*snap.ComponentInfo, error) {
		return &snap.ComponentInfo{
			Component:         csi.Component,
			Type:              componentNameToType(c, csi.Component.ComponentName),
			CompVersion:       "1.0",
			ComponentSideInfo: *csi,
		}, nil
	}))

	snapstate.Set(s.state, snapName, &snapstate.SnapState{
		Active:          true,
		Sequence:        currentSeq,
		Current:         si.Revision,
		SnapType:        string(snap.TypeKernel),
		TrackingChannel: channel,
	})

	ts, err := snapstate.UpdateOne(context.Background(), s.state, snapstate.StoreUpdateGoal(snapstate.StoreUpdate{
		InstanceName: snapName,
	}), nil, snapstate.Options{
		Flags: snapstate.Flags{
			NoReRefresh: true,
			Transaction: client.TransactionPerSnap,
		},
		UserID: s.user.ID,
	})
	c.Assert(err, IsNil)

	chg := s.state.NewChange("refresh", "refresh a snap")
	chg.AddAll(ts)

	if undo {
		last := lastWithLane(ts.Tasks())
		c.Assert(last, NotNil)

		terr := s.state.NewTask("error-trigger", "provoking total undo")
		terr.WaitFor(last)
		terr.JoinLane(last.Lanes()[0])
		chg.AddTask(terr)
	}

	// check unlink-reason
	unlinkTask := findLastTask(chg, "unlink-current-snap")
	c.Assert(unlinkTask, NotNil)
	var unlinkReason string
	unlinkTask.Get("unlink-reason", &unlinkReason)
	c.Check(unlinkReason, Equals, "refresh")

	// local modifications, edge must be set
	te := ts.MaybeEdge(snapstate.LastBeforeLocalModificationsEdge)
	c.Assert(te, NotNil)
	c.Assert(te.Kind(), Equals, "validate-component")

	// we manually settle here since this test can be slow when the host is
	// under load
	s.state.Unlock()
	err = s.o.Settle(testutil.HostScaledTimeout(15 * time.Second))
	s.state.Lock()
	c.Assert(err, IsNil, Commentf("change tasks:\n%s", printTasks(chg.Tasks())))

	if undo {
		c.Assert(chg.Err(), NotNil, Commentf("change tasks:\n%s", printTasks(chg.Tasks())))
	} else {
		c.Assert(chg.Err(), IsNil, Commentf("change tasks:\n%s", printTasks(chg.Tasks())))
	}

	expected := fakeOps{
		{
			op: "storesvc-snap-action",
			curSnaps: []store.CurrentSnap{{
				InstanceName:    snapName,
				SnapID:          snapID,
				Revision:        currentSnapRev,
				TrackingChannel: channel,
				RefreshedDate:   refreshedDate,
				Epoch:           snap.E("1*"),
				Resources:       currentResources,
			}},
			userID: 1,
		},
		{
			op: "storesvc-snap-action:action",
			action: store.SnapAction{
				Action:       "refresh",
				InstanceName: snapName,
				SnapID:       snapID,
				Channel:      channel,
				Flags:        store.SnapActionEnforceValidation,
			},
			revno:  newSnapRev,
			userID: 1,
		},
		{
			op:   "storesvc-download",
			name: snapName,
		},
		{
			op:    "validate-snap:Doing",
			name:  snapName,
			revno: newSnapRev,
		},
	}

	for _, cs := range expectedComponentStates {
		compName := cs.SideInfo.Component.ComponentName
		compRev := cs.SideInfo.Revision

		expected = append(expected, fakeOp{
			op:                "validate-component:Doing",
			name:              snapName,
			revno:             newSnapRev,
			componentName:     compName,
			componentRev:      compRev,
			componentSideInfo: *cs.SideInfo,
			// note that componentPath is empty here. that is because
			// download-component doesn't run, so it won't get set.
			// validate-component will use the location as if the component were
			// already downloaded.
			componentPath: "",
		})
	}

	expected = append(expected, fakeOps{
		{
			op:  "current",
			old: filepath.Join(dirs.SnapMountDir, snapName, currentSnapRev.String()),
		},
		{
			op:   "open-snap-file",
			path: filepath.Join(dirs.SnapBlobDir, fmt.Sprintf("%s_%v.snap", snapName, newSnapRev)),
			sinfo: snap.SideInfo{
				RealName: snapName,
				SnapID:   snapID,
				Channel:  channel,
				Revision: newSnapRev,
			},
		},
		{
			op:    "setup-snap",
			name:  snapName,
			path:  filepath.Join(dirs.SnapBlobDir, fmt.Sprintf("%s_%v.snap", snapName, newSnapRev)),
			revno: newSnapRev,
		},
		{
			op:          "run-inhibit-snap-for-unlink",
			name:        snapName,
			inhibitHint: "refresh",
		},
		{
			op:   "discard-namespace-locked",
			name: snapName,
		},
		{
			op:                 "unlink-snap",
			path:               filepath.Join(dirs.SnapMountDir, snapName, currentSnapRev.String()),
			unlinkSkipBinaries: true,
			inhibitHint:        "refresh",
		},
		{
			op: "prepare-kernel-snap",
		},
		{
			op:    "update-gadget-assets:Doing",
			name:  snapName,
			revno: newSnapRev,
		},
		{
			op:   "copy-data",
			path: filepath.Join(dirs.SnapMountDir, snapName, newSnapRev.String()),
			old:  filepath.Join(dirs.SnapMountDir, snapName, currentSnapRev.String()),
		},
		{
			op:   "setup-snap-save-data",
			path: filepath.Join(dirs.SnapDataSaveDir, snapName),
		},
	}...)

	expected = append(expected, fakeOps{
		{
			op:    "setup-profiles:Doing",
			name:  snapName,
			revno: newSnapRev,
		},
		{
			op: "candidate",
			sinfo: snap.SideInfo{
				RealName: snapName,
				SnapID:   snapID,
				Channel:  channel,
				Revision: newSnapRev,
			},
		},
		{
			op:                  "link-snap",
			path:                filepath.Join(dirs.SnapMountDir, snapName, newSnapRev.String()),
			requireSnapdTooling: true,
		},
	}...)

	for _, cs := range expectedComponentStates {
		compName := cs.SideInfo.Component.ComponentName
		compRev := cs.SideInfo.Revision
		expected = append(expected, fakeOp{
			op:   "link-component",
			path: snap.ComponentMountDir(compName, compRev, snapName),
		})
	}

	expected = append(expected, fakeOps{
		{
			op:    "auto-connect:Doing",
			name:  snapName,
			revno: newSnapRev,
		},
		{
			op: "update-aliases",
		},
	}...)

	currentKmodComps := make([]*snap.ComponentSideInfo, 0, len(currentComponentStates))
	for _, cs := range currentComponentStates {
		if cs.CompType == snap.KernelModulesComponent {
			currentKmodComps = append(currentKmodComps, cs.SideInfo)
		}
	}

	newKmodComps := make([]*snap.ComponentSideInfo, 0, len(expectedComponentStates))
	for _, cs := range expectedComponentStates {
		if cs.CompType == snap.KernelModulesComponent {
			newKmodComps = append(newKmodComps, cs.SideInfo)
		}
	}

	if len(currentKmodComps) > 0 || len(newKmodComps) > 0 {
		expected = append(expected, []fakeOp{{
			op:           "prepare-kernel-modules-components",
			currentComps: currentKmodComps,
			finalComps:   newKmodComps,
		}, {
			op: "maybe-set-next-boot",
		}, {
			op: "remove-kernel-snap-setup",
		}}...)
	}

	expectedSI := snap.SideInfo{
		RealName: snapName,
		Revision: newSnapRev,
		SnapID:   snapID,
		Channel:  channel,
	}
	expectedSideState := sequence.NewRevisionSideState(&expectedSI, expectedComponentStates)
	originalSideState := currentSeq.Revisions[0]

	if undo {
		expected = append(expected, undoOps(snapName, snap.TypeKernel, expectedSideState, originalSideState)...)
		expected = append(expected, fakeOp{
			op:   "storesvc-cleanup-download-artifacts",
			sha3: "<some-hash>",
			path: snap.MountFile(snapName, newSnapRev),
		})

	} else {
		expected = append(expected, fakeOp{
			op:    "cleanup-trash",
			name:  snapName,
			revno: newSnapRev,
		})
	}

	downloads := []fakeDownload{{
		macaroon: s.user.StoreMacaroon,
		name:     snapName,
		target:   filepath.Join(dirs.SnapBlobDir, fmt.Sprintf("%s_%v.snap", snapName, newSnapRev)),
	}}

	c.Check(s.fakeStore.downloads, DeepEquals, downloads)

	c.Check(s.fakeStore.seenPrivacyKeys["privacy-key"], Equals, true, Commentf("salts seen: %v", s.fakeStore.seenPrivacyKeys))

	// start with an easier-to-read error if this fails:
	c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
	c.Assert(s.fakeBackend.ops, DeepEquals, expected)

	// check progress
	task := ts.Tasks()[1]

	_, cur, total := task.Progress()
	c.Assert(cur, Equals, s.fakeStore.fakeCurrentProgress)
	c.Assert(total, Equals, s.fakeStore.fakeTotalProgress)

	// verify snapSetup info
	var snapsup snapstate.SnapSetup
	err = task.Get("snap-setup", &snapsup)
	c.Assert(err, IsNil)

	expectedSnapsup := snapstate.SnapSetup{
		Channel: channel,
		UserID:  s.user.ID,

		SideInfo:  snapsup.SideInfo,
		Type:      snap.TypeKernel,
		Version:   "kernelVer",
		PlugsOnly: true,
		Flags: snapstate.Flags{
			Transaction: client.TransactionPerSnap,
		},
		PreUpdateKernelModuleComponents: currentKmodComps,
		DownloadInfo: &snap.DownloadInfo{
			DownloadURL: "https://some-server.com/some/path.snap",
			Sha3_384:    "<some-hash>",
		},
	}

	c.Assert(snapsup, DeepEquals, expectedSnapsup)
	c.Assert(snapsup.SideInfo, DeepEquals, &snap.SideInfo{
		RealName: snapName,
		Revision: newSnapRev,
		Channel:  channel,
		SnapID:   snapID,
	})

	compsups := make([]snapstate.ComponentSetup, 0, len(expectedComponentStates))
	for _, cs := range expectedComponentStates {
		compName := cs.SideInfo.Component.ComponentName
		compsups = append(compsups, snapstate.ComponentSetup{
			CompSideInfo: cs.SideInfo,
			CompType:     cs.CompType,
			DownloadInfo: &snap.DownloadInfo{
				DownloadURL: "http://example.com/" + compName,
			},
			ComponentInstallFlags: snapstate.ComponentInstallFlags{
				MultiComponentInstall: true,
			},

			// once again note that CompPath is empty. that is because
			// download-component didn't run. this is safe because
			// mount-component doesn't run if the component is already mounted
			// and validate-component properly handles the case that this field
			// is empty.
			CompPath: "",
		})
	}
	checkComponentSetupTasks(c, ts, compsups, "prepare-component")

	// verify snaps in the system state
	var snapst snapstate.SnapState
	err = snapstate.Get(s.state, snapName, &snapst)
	c.Assert(err, IsNil)

	if !undo {
		c.Assert(snapst.LastRefreshTime, NotNil)
		c.Check(snapst.LastRefreshTime.Equal(now), Equals, true)
		c.Assert(snapst.Active, Equals, true)
		c.Assert(snapst.Sequence.Revisions, HasLen, 2)
		c.Assert(snapst.Sequence.Revisions[0], DeepEquals, currentSeq.Revisions[0])

		cand := sequence.NewRevisionSideState(&snap.SideInfo{
			RealName: snapName,
			Channel:  channel,
			SnapID:   snapID,
			Revision: newSnapRev,
		}, expectedComponentStates)

		currentSeq.Revisions = append(currentSeq.Revisions, cand)

		c.Assert(snapst.Sequence, DeepEquals, currentSeq)

		// we end up with the auxiliary store info
		c.Check(backend.AuxStoreInfoFilename(snapID), testutil.FilePresent)
	} else {
		// make sure everything is back to how it started
		c.Assert(snapst.Active, Equals, true)
		c.Assert(snapst.Sequence.Revisions, HasLen, 1)
		c.Assert(snapst.Sequence.Revisions[0], DeepEquals, currentSeq.Revisions[0])
	}
}

func (s *snapmgrTestSuite) TestRefreshCandidates(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:          true,
		TrackingChannel: "latest/stable",
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(2)},
		}),
		Current:  snap.R(2),
		SnapType: "app",
	})

	snapstate.Set(s.state, "some-other-snap", &snapstate.SnapState{
		Active:          true,
		TrackingChannel: "channel-for-7/stable",
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{
			{RealName: "some-other-snap", SnapID: "some-other-snap-id", Revision: snap.R(7)},
		}),
		Current:  snap.R(7),
		SnapType: "app",
	})

	candidates, err := snapstate.RefreshCandidates(s.state, nil)
	c.Assert(err, IsNil)
	c.Assert(candidates, HasLen, 1)
	c.Check(candidates[0].InstanceName(), Equals, "some-snap")
}

func (s *snapmgrTestSuite) TestUpdateTasksWithComponentsRemoved(c *C) {
	s.state.Lock()
	defer s.state.Unlock()
	restore := release.MockOnClassic(false)
	defer restore()

	si1 := &snap.SideInfo{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)}
	si2 := &snap.SideInfo{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(2)}
	si3 := &snap.SideInfo{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(3)}
	si4 := &snap.SideInfo{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(4)}
	si5 := &snap.SideInfo{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(5)}
	cref1 := naming.NewComponentRef("some-snap", "comp1")
	cref2 := naming.NewComponentRef("some-snap", "comp2")
	comp1si := snap.NewComponentSideInfo(cref1, snap.R(11))
	comp2si := snap.NewComponentSideInfo(cref2, snap.R(22))
	s.AddCleanup(snapstate.MockReadComponentInfo(func(compMntDir string,
		snapInfo *snap.Info, csi *snap.ComponentSideInfo) (*snap.ComponentInfo, error) {
		switch csi.Component.ComponentName {
		case "comp1":
			return &snap.ComponentInfo{
				Component:         cref1,
				Type:              snap.StandardComponent,
				ComponentSideInfo: *csi,
			}, nil
		case "comp2":
			return &snap.ComponentInfo{
				Component:         cref2,
				Type:              snap.StandardComponent,
				ComponentSideInfo: *csi,
			}, nil
		}
		return nil, errors.New("unexpected component")
	}))
	seq := snapstatetest.NewSequenceFromRevisionSideInfos(
		[]*sequence.RevisionSideState{
			sequence.NewRevisionSideState(si1,
				[]*sequence.ComponentState{
					sequence.NewComponentState(
						comp1si, snap.StandardComponent),
					sequence.NewComponentState(
						comp2si, snap.StandardComponent),
				}),
			sequence.NewRevisionSideState(si2, nil),
			sequence.NewRevisionSideState(si3, nil),
			sequence.NewRevisionSideState(si4,
				[]*sequence.ComponentState{
					sequence.NewComponentState(
						comp1si, snap.StandardComponent),
					sequence.NewComponentState(
						comp2si, snap.StandardComponent),
				}),
			sequence.NewRevisionSideState(si5, nil),
		})

	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:          true,
		TrackingChannel: "latest/edge",
		Sequence:        seq,
		Current:         snap.R(3),
		SnapType:        "app",
	})

	// run the update
	ts, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "some-channel"}, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)

	c.Assert(taskKinds(ts.Tasks()), DeepEquals, []string{
		"prerequisites",
		"download-snap",
		"validate-snap",
		"prerequisites",
		"mount-snap",
		"run-hook[pre-refresh]",
		"stop-snap-services",
		"remove-aliases",
		"unlink-current-snap",
		"copy-snap-data",
		"setup-profiles",
		"link-snap",
		"auto-connect",
		"set-auto-aliases",
		"setup-aliases",
		"run-hook[post-refresh]",
		"start-snap-services",
		"clear-snap",
		"unlink-component",
		"discard-component",
		"unlink-component",
		"discard-component",
		"discard-snap",
		"clear-snap",
		"discard-snap",
		"clear-snap",
		"unlink-component",
		"discard-component",
		"unlink-component",
		"discard-component",
		"discard-snap",
		"cleanup",
		"run-hook[configure]",
		"run-hook[check-health]",
		"check-rerefresh",
		"mock-process-delayed-security-backend-effects",
	})

	// and ensure that it will remove the components - si1 is cleaned
	// because of garbage collection and si4 and si5 because they are after
	// current.
	var compSup snapstate.ComponentSetup
	tasks := ts.Tasks()

	// TODO: use a helper to find required tasks
	i := len(tasks) - 18
	c.Check(tasks[i].Kind(), Equals, "unlink-component")
	err = tasks[i].Get("component-setup", &compSup)
	c.Assert(err, IsNil)
	c.Check(compSup.CompSideInfo.Component, Equals, cref1)

	i = len(tasks) - 16
	c.Check(tasks[i].Kind(), Equals, "unlink-component")
	err = tasks[i].Get("component-setup", &compSup)
	c.Assert(err, IsNil)
	c.Check(compSup.CompSideInfo.Component, Equals, cref2)

	i = len(tasks) - 10
	c.Check(tasks[i].Kind(), Equals, "unlink-component")
	err = tasks[i].Get("component-setup", &compSup)
	c.Assert(err, IsNil)
	c.Check(compSup.CompSideInfo.Component, Equals, cref1)

	i = len(tasks) - 8
	c.Check(tasks[i].Kind(), Equals, "unlink-component")
	err = tasks[i].Get("component-setup", &compSup)
	c.Assert(err, IsNil)
	c.Check(compSup.CompSideInfo.Component, Equals, cref2)
}

func (s *snapmgrTestSuite) TestUpdateWithComponentsFromPathRunThrough(c *C) {
	const (
		instanceKey           = ""
		snapType              = snap.TypeKernel
		refreshAppAwarenessUX = true
		undo                  = false
	)
	s.testUpdateWithComponentsFromPathRunThrough(c, instanceKey, snapType,
		[]string{"standard-component", "kernel-modules-component"}, refreshAppAwarenessUX, undo)
}

func (s *snapmgrTestSuite) TestUpdateWithComponentsFromPathRunThroughUndo(c *C) {
	const (
		instanceKey           = ""
		snapType              = snap.TypeKernel
		refreshAppAwarenessUX = true
		undo                  = true
	)
	s.testUpdateWithComponentsFromPathRunThrough(c, instanceKey, snapType,
		[]string{"standard-component", "kernel-modules-component"}, refreshAppAwarenessUX, undo)
}

func (s *snapmgrTestSuite) TestUpdateInstanceWithComponentsFromPathRunThrough(c *C) {
	const (
		instanceKey           = "key"
		snapType              = snap.TypeApp
		refreshAppAwarenessUX = true
		undo                  = false
	)
	s.testUpdateWithComponentsFromPathRunThrough(c, instanceKey, snapType,
		[]string{"standard-component", "standard-component-extra"}, refreshAppAwarenessUX, undo)
}

func (s *snapmgrTestSuite) TestUpdateInstanceWithComponentsFromPathRunThroughUndo(c *C) {
	const (
		instanceKey           = "key"
		snapType              = snap.TypeApp
		refreshAppAwarenessUX = true
		undo                  = true
	)
	s.testUpdateWithComponentsFromPathRunThrough(c, instanceKey, snapType,
		[]string{"standard-component", "standard-component-extra"}, refreshAppAwarenessUX, undo)
}

func (s *snapmgrTestSuite) testUpdateWithComponentsFromPathRunThrough(c *C, instanceKey string, snapType snap.Type, compNames []string, refreshAppAwarenessUX, undo bool) {
	// use the real thing for this one
	snapstate.MockOpenSnapFile(backend.OpenSnapFile)

	const (
		channel = "channel-for-components"
	)
	var snapName, snapID string
	switch snapType {
	case snap.TypeKernel:
		snapName = "kernel"
		snapID = "kernel-id"
	case snap.TypeApp:
		snapName = "some-snap"
		snapID = "some-snap-id"
	}

	r := snapstatetest.MockDeviceModel(MakeModel20("pc", map[string]any{"base": "core24"}))
	defer r()

	if refreshAppAwarenessUX {
		s.enableRefreshAppAwarenessUX()
	}

	currentSnapRev := snap.R(7)
	newSnapRev := snap.R(11)
	instanceName := snap.InstanceName(snapName, instanceKey)

	sort.Strings(compNames)

	si := snap.SideInfo{
		RealName: snapName,
		Revision: currentSnapRev,
		SnapID:   snapID,
		Channel:  channel,
	}

	snaptest.MockSnapInstance(c, instanceName, fmt.Sprintf("name: %s\ntype: %s\n", snapName, snapType), &si)

	bl := boottest.MockUC20RunBootenv(bootloadertest.Mock("mock", c.MkDir()))
	bl.SetBootVars(map[string]string{"snap_kernel": snapName, "snap_core": "core24_2.snap"})
	bootloader.Force(bl)
	defer bootloader.Force(nil)

	// This is the new revision as this information is accessed after the reboot
	kPlaceInfo, err := snap.ParsePlaceInfoFromSnapFileName("kernel_" + newSnapRev.String() + ".snap")
	c.Assert(err, IsNil)
	restore := bl.SetEnabledKernel(kPlaceInfo)
	defer restore()

	restore = snapstate.MockRevisionDate(nil)
	defer restore()

	now, err := time.Parse(time.RFC3339, "2021-06-10T10:00:00Z")
	c.Assert(err, IsNil)

	restore = snapstate.MockTimeNow(func() time.Time {
		return now
	})
	defer restore()

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

	// make sure that the store is never hit
	snapstate.ReplaceStore(s.state, &storetest.Store{})

	if instanceKey != "" {
		tr := config.NewTransaction(s.state)
		tr.Set("core", "experimental.parallel-instances", true)
		tr.Commit()
	}

	currentSeq := snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si})

	originalCompRevisions := make(map[string]snap.Revision)
	for i, compName := range compNames {
		originalCompRevisions[compName] = snap.R(i + 1)
	}

	updatedCompRevisions := make(map[string]snap.Revision)
	for _, compName := range compNames {
		updatedCompRevisions[compName] = snap.R(originalCompRevisions[compName].N + 1)
	}

	expectedComponentStates := make([]*sequence.ComponentState, 0, len(compNames))
	for _, comp := range compNames {
		expectedComponentStates = append(expectedComponentStates, &sequence.ComponentState{
			SideInfo: &snap.ComponentSideInfo{
				Component: naming.NewComponentRef(snapName, comp),
				Revision:  updatedCompRevisions[comp],
			},
			CompType: componentNameToType(c, comp),
		})
	}

	expectedSI := snap.SideInfo{
		RealName: snapName,
		Revision: newSnapRev,
		SnapID:   snapID,
		Channel:  channel,
	}
	expectedSideState := sequence.NewRevisionSideState(&expectedSI, expectedComponentStates)
	originalSideState := currentSeq.Revisions[0]

	for _, comp := range compNames {
		err := currentSeq.AddComponentForRevision(currentSnapRev, &sequence.ComponentState{
			SideInfo: &snap.ComponentSideInfo{
				Component: naming.NewComponentRef(snapName, comp),
				Revision:  originalCompRevisions[comp],
			},
			CompType: componentNameToType(c, comp),
		})
		c.Assert(err, IsNil)
	}

	s.AddCleanup(snapstate.MockReadComponentInfo(func(
		compMntDir string, info *snap.Info, csi *snap.ComponentSideInfo,
	) (*snap.ComponentInfo, error) {
		return &snap.ComponentInfo{
			Component:         csi.Component,
			Type:              componentNameToType(c, csi.Component.ComponentName),
			CompVersion:       "1.0",
			ComponentSideInfo: *csi,
		}, nil
	}))

	snapstate.Set(s.state, instanceName, &snapstate.SnapState{
		Active:          true,
		Sequence:        currentSeq,
		Current:         si.Revision,
		SnapType:        string(snapType),
		TrackingChannel: channel,
		InstanceKey:     instanceKey,
	})

	components := make([]snapstate.PathComponent, 0, len(compNames))
	componentPaths := make(map[string]string, len(compNames))
	for _, cs := range expectedComponentStates {
		componentYaml := fmt.Sprintf(`component: %s
type: %s
version: 1.0
`, cs.SideInfo.Component, cs.CompType)

		path := snaptest.MakeTestComponent(c, componentYaml)
		componentPaths[cs.SideInfo.Component.ComponentName] = path
		components = append(components, snapstate.PathComponent{
			Path:     path,
			SideInfo: cs.SideInfo,
		})
	}

	var snapPath string
	switch snapType {
	case snap.TypeKernel:
		snapPath = makeTestSnap(c, `name: kernel
version: some-snapVer
type: kernel
epoch: 1*
components:
  standard-component:
    type: standard
  kernel-modules-component:
    type: kernel-modules
`)
	case snap.TypeApp:
		snapPath = makeTestSnap(c, `name: some-snap
version: some-snapVer
type: app
epoch: 1*
components:
  standard-component:
    type: standard
  standard-component-extra:
    type: standard
`)
	}

	newSI := si
	newSI.Revision = newSnapRev

	goal := snapstate.PathUpdateGoal(snapstate.PathSnap{
		Path:         snapPath,
		InstanceName: instanceName,
		SideInfo:     &newSI,
		Components:   components,
	})

	ts, err := snapstate.UpdateOne(context.Background(), s.state, goal, nil, snapstate.Options{
		Flags: snapstate.Flags{
			Transaction:    client.TransactionPerSnap,
			RemoveSnapPath: true,
		},
	})
	c.Assert(err, IsNil)

	chg := s.state.NewChange("refresh", "refresh a snap")
	chg.AddAll(ts)

	if undo {
		last := lastWithLane(ts.Tasks())
		c.Assert(last, NotNil)

		terr := s.state.NewTask("error-trigger", "provoking total undo")
		terr.WaitFor(last)
		terr.JoinLane(last.Lanes()[0])
		chg.AddTask(terr)
	}

	// check unlink-reason
	unlinkTask := findLastTask(chg, "unlink-current-snap")
	c.Assert(unlinkTask, NotNil)
	var unlinkReason string
	unlinkTask.Get("unlink-reason", &unlinkReason)
	c.Check(unlinkReason, Equals, "refresh")

	// local modifications, edge must be set
	te := ts.MaybeEdge(snapstate.LastBeforeLocalModificationsEdge)
	c.Assert(te, NotNil)
	c.Assert(te.Kind(), Equals, "validate-component")

	// we manually settle here since this test can be slow when the host is
	// under load
	s.state.Unlock()
	err = s.o.Settle(testutil.HostScaledTimeout(15 * time.Second))
	s.state.Lock()
	c.Assert(err, IsNil, Commentf("change tasks:\n%s", printTasks(chg.Tasks())))

	if undo {
		c.Assert(chg.Err(), NotNil, Commentf("change tasks:\n%s", printTasks(chg.Tasks())))
	} else {
		c.Assert(chg.Err(), IsNil, Commentf("change tasks:\n%s", printTasks(chg.Tasks())))
	}

	var expected fakeOps
	for _, cs := range expectedComponentStates {
		compName := cs.SideInfo.Component.ComponentName
		compRev := cs.SideInfo.Revision
		expected = append(expected, fakeOp{
			op:                              "validate-component:Doing",
			name:                            instanceName,
			revno:                           newSnapRev,
			componentName:                   compName,
			componentPath:                   componentPaths[cs.SideInfo.Component.ComponentName],
			componentRev:                    compRev,
			componentSideInfo:               *cs.SideInfo,
			componentSkipAssertionsDownload: true,
		})
	}

	expected = append(expected, fakeOps{
		{
			op:  "current",
			old: filepath.Join(dirs.SnapMountDir, instanceName, currentSnapRev.String()),
		},
		{
			op:    "setup-snap",
			name:  instanceName,
			path:  snapPath,
			revno: newSnapRev,
		},
	}...)

	if !refreshAppAwarenessUX {
		expected = append(expected, fakeOp{
			op:   "remove-snap-aliases",
			name: instanceName,
		})
	}

	for _, cs := range expectedComponentStates {
		compName := cs.SideInfo.Component.ComponentName
		compRev := cs.SideInfo.Revision
		containerName := fmt.Sprintf("%s+%s", instanceName, compName)
		filename := fmt.Sprintf("%s_%v.comp", containerName, compRev)

		expected = append(expected, fakeOp{
			op:                "setup-component",
			containerName:     containerName,
			containerFileName: filename,
		})
	}

	expected = append(expected, fakeOps{
		{
			op:          "run-inhibit-snap-for-unlink",
			name:        instanceName,
			inhibitHint: "refresh",
		},
		{
			op:   "discard-namespace-locked",
			name: instanceName,
		},
		{
			op:                 "unlink-snap",
			path:               filepath.Join(dirs.SnapMountDir, instanceName, currentSnapRev.String()),
			unlinkSkipBinaries: refreshAppAwarenessUX,
			inhibitHint:        "refresh",
		}}...)
	if snapType == snap.TypeKernel {
		expected = append(expected,
			fakeOp{
				op: "prepare-kernel-snap",
			}, fakeOp{
				op:    "update-gadget-assets:Doing",
				name:  instanceName,
				revno: newSnapRev,
			})
	}
	expected = append(expected, fakeOps{
		{
			op:   "copy-data",
			path: filepath.Join(dirs.SnapMountDir, instanceName, newSnapRev.String()),
			old:  filepath.Join(dirs.SnapMountDir, instanceName, currentSnapRev.String()),
		},
		{
			op:   "setup-snap-save-data",
			path: filepath.Join(dirs.SnapDataSaveDir, instanceName),
		},
	}...)

	expected = append(expected, fakeOps{
		{
			op:    "setup-profiles:Doing",
			name:  instanceName,
			revno: snap.R(11),
		},
		{
			op: "candidate",
			sinfo: snap.SideInfo{
				RealName: snapName,
				SnapID:   snapID,
				Channel:  channel,
				Revision: snap.R(11),
			},
		},
		{
			op:                  "link-snap",
			path:                filepath.Join(dirs.SnapMountDir, instanceName, newSnapRev.String()),
			requireSnapdTooling: true,
		},
	}...)

	currentKmodComps := make([]*snap.ComponentSideInfo, 0, len(components))
	newKmodComps := make([]*snap.ComponentSideInfo, 0, len(components))
	for _, cs := range expectedComponentStates {
		if cs.CompType == snap.KernelModulesComponent {
			compName := cs.SideInfo.Component.ComponentName
			currentKmodComps = append(currentKmodComps, &snap.ComponentSideInfo{
				Component: naming.NewComponentRef(snapName, compName),
				Revision:  originalCompRevisions[compName],
			})
			newKmodComps = append(newKmodComps, &snap.ComponentSideInfo{
				Component: naming.NewComponentRef(snapName, compName),
				Revision:  cs.SideInfo.Revision,
			})
		}
	}

	if len(currentKmodComps) == 0 && len(newKmodComps) == 0 {
		expected = append(expected, fakeOp{
			op: "maybe-set-next-boot",
		})
	}

	for _, cs := range expectedComponentStates {
		expected = append(expected, fakeOp{
			op:   "link-component",
			path: snap.ComponentMountDir(cs.SideInfo.Component.ComponentName, cs.SideInfo.Revision, instanceName),
		})
	}

	expected = append(expected, fakeOps{
		{
			op:    "auto-connect:Doing",
			name:  instanceName,
			revno: snap.R(11),
		},
		{
			op: "update-aliases",
		},
	}...)

	if len(currentKmodComps) > 0 || len(newKmodComps) > 0 {
		expected = append(expected, fakeOp{
			op:           "prepare-kernel-modules-components",
			currentComps: currentKmodComps,
			finalComps:   newKmodComps,
		})
	}

	if snapType == snap.TypeKernel {
		expected = append(expected, fakeOp{
			op: "maybe-set-next-boot",
		}, fakeOp{
			op: "remove-kernel-snap-setup",
		})
	}

	if undo {
		expected = append(expected, undoOps(instanceName, snapType, expectedSideState, originalSideState)...)
	} else {
		expected = append(expected, fakeOp{
			op:    "cleanup-trash",
			name:  instanceName,
			revno: newSnapRev,
		})
	}

	c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
	c.Assert(s.fakeBackend.ops, DeepEquals, expected)

	// check progress
	task := ts.Tasks()[1]

	// verify snapSetup info
	var snapsup snapstate.SnapSetup
	err = task.Get("snap-setup", &snapsup)
	c.Assert(err, IsNil)
	c.Assert(snapsup, DeepEquals, snapstate.SnapSetup{
		Channel:   channel,
		SideInfo:  snapsup.SideInfo,
		Type:      snapType,
		Version:   "some-snapVer",
		PlugsOnly: true,
		Flags: snapstate.Flags{
			Transaction:    client.TransactionPerSnap,
			RemoveSnapPath: true,
		},
		InstanceKey:                     instanceKey,
		PreUpdateKernelModuleComponents: currentKmodComps,
	})
	c.Assert(snapsup.SideInfo, DeepEquals, &snap.SideInfo{
		RealName: snapName,
		Revision: newSnapRev,
		Channel:  channel,
		SnapID:   snapID,
	})

	compsups := make([]snapstate.ComponentSetup, 0, len(compNames))
	for i, comp := range compNames {
		compsups = append(compsups, snapstate.ComponentSetup{
			CompSideInfo: &snap.ComponentSideInfo{
				Component: naming.NewComponentRef(snapName, comp),
				Revision:  snap.R(i + 2),
			},
			CompType: componentNameToType(c, comp),
			CompPath: componentPaths[comp],
			ComponentInstallFlags: snapstate.ComponentInstallFlags{
				RemoveComponentPath:   true,
				MultiComponentInstall: true,
			},
			SkipAssertionsDownload: true,
		})
	}
	checkComponentSetupTasks(c, ts, compsups, "prepare-component")

	// verify snaps in the system state
	var snapst snapstate.SnapState
	err = snapstate.Get(s.state, instanceName, &snapst)
	c.Assert(err, IsNil)

	if !undo {
		c.Assert(snapst.LastRefreshTime, NotNil)
		c.Check(snapst.LastRefreshTime.Equal(now), Equals, true)
		c.Assert(snapst.Active, Equals, true)
		c.Assert(snapst.Sequence.Revisions, HasLen, 2)
		c.Assert(snapst.Sequence.Revisions[0], DeepEquals, currentSeq.Revisions[0])

		cand := sequence.NewRevisionSideState(&snap.SideInfo{
			RealName: snapName,
			Channel:  channel,
			SnapID:   snapID,
			Revision: newSnapRev,
		}, expectedComponentStates)

		// add our new revision to the sequence
		currentSeq.Revisions = append(currentSeq.Revisions, cand)

		c.Assert(snapst.Sequence, DeepEquals, currentSeq)

		c.Check(snapPath, testutil.FileAbsent)
		for _, comp := range components {
			c.Check(comp.Path, testutil.FileAbsent)
		}
	} else {
		// make sure everything is back to how it started
		c.Assert(snapst.Active, Equals, true)
		c.Assert(snapst.Sequence.Revisions, HasLen, 1)
		c.Assert(snapst.Sequence.Revisions[0], DeepEquals, currentSeq.Revisions[0])
	}
}

func withComponents(yaml string, comps map[string]snap.ComponentType) string {
	if len(comps) == 0 {
		return yaml
	}

	var b strings.Builder
	b.WriteString(yaml)
	b.WriteString("\ncomponents:")
	for name, typ := range comps {
		fmt.Fprintf(&b, "\n  %s:\n    type: %s", name, typ)
	}
	return b.String()
}

func (s *snapmgrTestSuite) TestUpdateWithComponentsFromPathBackToInstalledRevision(c *C) {
	// use the real thing for this one
	snapstate.MockOpenSnapFile(backend.OpenSnapFile)

	const (
		channel      = "channel-for-components"
		snapName     = "kernel"
		instanceName = snapName
		snapID       = "kernel-id"
	)

	r := snapstatetest.MockDeviceModel(MakeModel20("pc", map[string]any{"base": "core24"}))
	defer r()

	s.enableRefreshAppAwarenessUX()

	oldSnapRev := snap.R(7)
	currentSnapRev := snap.R(11)

	currentSI := snap.SideInfo{
		RealName: snapName,
		Revision: currentSnapRev,
		SnapID:   snapID,
		Channel:  channel,
	}

	oldSI := snap.SideInfo{
		RealName: snapName,
		Revision: oldSnapRev,
		SnapID:   snapID,
		Channel:  channel,
	}

	currentYAML := fmt.Sprintf("name: %s\ntype: %s\nversion: 1\nepoch: 1*\n", snapName, snap.TypeKernel)
	snaptest.MockSnapInstance(c, instanceName, currentYAML, &currentSI)

	oldYAML := withComponents(currentYAML, map[string]snap.ComponentType{
		"kernel-modules-component": snap.KernelModulesComponent,
	})
	oldInfo := snaptest.MockSnapInstance(c, instanceName, oldYAML, &oldSI)
	oldComponentSI := snap.ComponentSideInfo{
		Component: naming.NewComponentRef(snapName, "kernel-modules-component"),
		Revision:  snap.R(22),
	}

	snaptest.MockComponent(c, fmt.Sprintf("component: %s+kernel-modules-component\ntype: kernel-modules\n", snapName), oldInfo, oldComponentSI)

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

	snapstatetest.InstallSnap(c, s.state, oldYAML, nil, &oldSI, snapstatetest.InstallSnapOptions{
		Required: true,
		Components: []*sequence.ComponentState{
			sequence.NewComponentState(&oldComponentSI, snap.KernelModulesComponent),
		},
	})

	snapstatetest.InstallSnap(c, s.state, currentYAML, nil, &currentSI, snapstatetest.InstallSnapOptions{
		Required:         true,
		PreserveSequence: true,
	})

	var snapst snapstate.SnapState
	err := snapstate.Get(s.state, instanceName, &snapst)
	c.Assert(err, IsNil)

	bl := boottest.MockUC20RunBootenv(bootloadertest.Mock("mock", c.MkDir()))
	bl.SetBootVars(map[string]string{"snap_kernel": snapName, "snap_core": "core24_2.snap"})
	bootloader.Force(bl)
	defer bootloader.Force(nil)

	// This is the new revision as this information is accessed after the reboot
	kPlaceInfo, err := snap.ParsePlaceInfoFromSnapFileName("kernel_" + oldSnapRev.String() + ".snap")
	c.Assert(err, IsNil)
	restore := bl.SetEnabledKernel(kPlaceInfo)
	defer restore()

	restore = snapstate.MockRevisionDate(nil)
	defer restore()

	now, err := time.Parse(time.RFC3339, "2021-06-10T10:00:00Z")
	c.Assert(err, IsNil)

	restore = snapstate.MockTimeNow(func() time.Time {
		return now
	})
	defer restore()

	// make sure that the store is never hit
	snapstate.ReplaceStore(s.state, &storetest.Store{})

	expectedSideState := snapst.Sequence.Revisions[0]
	originalSideState := snapst.Sequence.Revisions[1]

	s.AddCleanup(snapstate.MockReadComponentInfo(func(
		compMntDir string, info *snap.Info, csi *snap.ComponentSideInfo,
	) (*snap.ComponentInfo, error) {
		return &snap.ComponentInfo{
			Component:         csi.Component,
			Type:              componentNameToType(c, csi.Component.ComponentName),
			CompVersion:       "1.0",
			ComponentSideInfo: *csi,
		}, nil
	}))

	oldComponentCPI := snap.MinimalComponentContainerPlaceInfo(
		"kernel-modules-component",
		oldComponentSI.Revision,
		snapName,
	)

	components := []snapstate.PathComponent{{
		Path:     oldComponentCPI.MountFile(),
		SideInfo: &oldComponentSI,
	}}

	oldSnapPath := oldInfo.MountFile()
	goal := snapstate.PathUpdateGoal(snapstate.PathSnap{
		Path:         oldSnapPath,
		InstanceName: instanceName,
		SideInfo:     &oldSI,
		Components:   components,
	})

	ts, err := snapstate.UpdateOne(context.Background(), s.state, goal, nil, snapstate.Options{
		Flags: snapstate.Flags{
			Transaction: client.TransactionPerSnap,
		},
	})
	c.Assert(err, IsNil)

	chg := s.state.NewChange("refresh", "refresh a snap")
	chg.AddAll(ts)

	// check unlink-reason
	unlinkTask := findLastTask(chg, "unlink-current-snap")
	c.Assert(unlinkTask, NotNil)
	var unlinkReason string
	unlinkTask.Get("unlink-reason", &unlinkReason)
	c.Check(unlinkReason, Equals, "refresh")

	// local modifications, edge must be set
	te := ts.MaybeEdge(snapstate.LastBeforeLocalModificationsEdge)
	c.Assert(te, NotNil)
	c.Assert(te.Kind(), Equals, "validate-component")

	// we manually settle here since this test can be slow when the host is
	// under load
	s.state.Unlock()
	err = s.o.Settle(testutil.HostScaledTimeout(15 * time.Second))
	s.state.Lock()
	c.Assert(err, IsNil, Commentf("change tasks:\n%s", printTasks(chg.Tasks())))

	c.Assert(chg.Err(), IsNil, Commentf("change tasks:\n%s", printTasks(chg.Tasks())))

	expectedComponentStates := snapst.Sequence.Revisions[0].Components

	var expected fakeOps
	for _, cs := range expectedComponentStates {
		compName := cs.SideInfo.Component.ComponentName
		compRev := cs.SideInfo.Revision
		cpi := snap.MinimalComponentContainerPlaceInfo(
			compName,
			compRev,
			snapName,
		)

		expected = append(expected, fakeOp{
			op:                              "validate-component:Doing",
			name:                            instanceName,
			revno:                           oldSnapRev,
			componentName:                   compName,
			componentPath:                   cpi.MountFile(),
			componentRev:                    compRev,
			componentSideInfo:               *cs.SideInfo,
			componentSkipAssertionsDownload: true,
		})
	}

	expected = append(expected, fakeOps{
		{
			op:          "run-inhibit-snap-for-unlink",
			name:        instanceName,
			inhibitHint: "refresh",
		},
		{
			op:   "discard-namespace-locked",
			name: instanceName,
		},
		{
			op:                 "unlink-snap",
			path:               filepath.Join(dirs.SnapMountDir, instanceName, currentSnapRev.String()),
			unlinkSkipBinaries: true,
			inhibitHint:        "refresh",
		},
		{
			op: "prepare-kernel-snap",
		},
		{
			op:    "update-gadget-assets:Doing",
			name:  instanceName,
			revno: oldSnapRev,
		},
		{
			op:   "copy-data",
			path: filepath.Join(dirs.SnapMountDir, instanceName, oldSnapRev.String()),
			old:  filepath.Join(dirs.SnapMountDir, instanceName, currentSnapRev.String()),
		},
		{
			op:   "setup-snap-save-data",
			path: filepath.Join(dirs.SnapDataSaveDir, instanceName),
		},
		{
			op:    "setup-profiles:Doing",
			name:  instanceName,
			revno: oldSnapRev,
		},
		{
			op: "candidate",
			sinfo: snap.SideInfo{
				RealName: snapName,
				SnapID:   snapID,
				Channel:  channel,
				Revision: oldSnapRev,
			},
		},
		{
			op:                  "link-snap",
			path:                filepath.Join(dirs.SnapMountDir, instanceName, oldSnapRev.String()),
			requireSnapdTooling: true,
		},
	}...)

	currentKmodComps := make([]*snap.ComponentSideInfo, 0, len(components))
	newKmodComps := make([]*snap.ComponentSideInfo, 0, len(components))
	for _, cs := range expectedComponentStates {
		if cs.CompType == snap.KernelModulesComponent {
			compName := cs.SideInfo.Component.ComponentName
			newKmodComps = append(newKmodComps, &snap.ComponentSideInfo{
				Component: naming.NewComponentRef(snapName, compName),
				Revision:  cs.SideInfo.Revision,
			})
		}
	}

	if len(currentKmodComps) == 0 && len(newKmodComps) == 0 {
		expected = append(expected, fakeOp{
			op: "maybe-set-next-boot",
		})
	}

	expected = append(expected, fakeOps{
		{
			op:    "auto-connect:Doing",
			name:  instanceName,
			revno: oldSnapRev,
		},
		{
			op: "update-aliases",
		},
	}...)

	if len(currentKmodComps) > 0 || len(newKmodComps) > 0 {
		expected = append(expected, fakeOp{
			op:           "prepare-kernel-modules-components",
			currentComps: currentKmodComps,
			finalComps:   newKmodComps,
		})
	}

	expected = append(expected, fakeOp{
		op: "maybe-set-next-boot",
	}, fakeOp{
		op: "remove-kernel-snap-setup",
	}, fakeOp{
		op:    "cleanup-trash",
		name:  instanceName,
		revno: oldSnapRev,
	})

	c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
	c.Assert(s.fakeBackend.ops, DeepEquals, expected)

	// check progress
	task := ts.Tasks()[1]

	// verify snapSetup info
	var snapsup snapstate.SnapSetup
	err = task.Get("snap-setup", &snapsup)
	c.Assert(err, IsNil)
	c.Assert(snapsup, DeepEquals, snapstate.SnapSetup{
		Channel:   channel,
		SnapPath:  oldSnapPath,
		SideInfo:  snapsup.SideInfo,
		Type:      snap.TypeKernel,
		Version:   "1",
		PlugsOnly: true,
		Flags: snapstate.Flags{
			Transaction: client.TransactionPerSnap,
		},
		PreUpdateKernelModuleComponents: currentKmodComps,
	})
	c.Assert(snapsup.SideInfo, DeepEquals, &snap.SideInfo{
		RealName: snapName,
		Revision: oldSnapRev,
		Channel:  channel,
		SnapID:   snapID,
	})

	compsups := make([]snapstate.ComponentSetup, 0)
	for _, comp := range expectedComponentStates {
		cpi := snap.MinimalComponentContainerPlaceInfo(
			comp.SideInfo.Component.ComponentName,
			comp.SideInfo.Revision,
			snapName,
		)

		compsups = append(compsups, snapstate.ComponentSetup{
			CompSideInfo: &snap.ComponentSideInfo{
				Component: comp.SideInfo.Component,
				Revision:  comp.SideInfo.Revision,
			},
			CompType: componentNameToType(c, comp.SideInfo.Component.ComponentName),
			CompPath: cpi.MountFile(),
			ComponentInstallFlags: snapstate.ComponentInstallFlags{
				MultiComponentInstall: true,
			},
			SkipAssertionsDownload: true,
		})
	}
	checkComponentSetupTasks(c, ts, compsups, "prepare-component")

	// verify the new system state
	err = snapstate.Get(s.state, instanceName, &snapst)
	c.Assert(err, IsNil)

	c.Assert(snapst.LastRefreshTime, NotNil)
	c.Check(snapst.LastRefreshTime.Equal(now), Equals, true)
	c.Assert(snapst.Active, Equals, true)
	c.Assert(snapst.Sequence.Revisions, HasLen, 2)
	c.Assert(snapst.Sequence.Revisions[0], DeepEquals, originalSideState)
	c.Assert(snapst.Sequence.Revisions[1], DeepEquals, expectedSideState)
}

func (s *snapmgrTestSuite) TestUpdateWithComponentsRunThroughOnlyComponentUpdate(c *C) {
	s.testUpdateWithComponentsRunThroughOnlyComponentUpdate(c, updateWithComponentsOpts{
		snapType:              snap.TypeKernel,
		components:            []string{"standard-component", "kernel-modules-component"},
		refreshAppAwarenessUX: true,
	})
}

func (s *snapmgrTestSuite) TestUpdateWithComponentsRunThroughOnlyComponentUpdateUndo(c *C) {
	s.testUpdateWithComponentsRunThroughOnlyComponentUpdate(c, updateWithComponentsOpts{
		snapType:              snap.TypeKernel,
		components:            []string{"standard-component", "kernel-modules-component"},
		refreshAppAwarenessUX: true,
		undo:                  true,
	})
}

func (s *snapmgrTestSuite) TestUpdateInstanceWithComponentsRunThroughOnlyComponentUpdate(c *C) {
	s.testUpdateWithComponentsRunThroughOnlyComponentUpdate(c, updateWithComponentsOpts{
		snapType:              snap.TypeApp,
		instanceKey:           "key",
		components:            []string{"standard-component", "standard-component-extra"},
		refreshAppAwarenessUX: true,
	})
}

func (s *snapmgrTestSuite) TestUpdateInstanceWithComponentsRunThroughOnlyComponentUpdateUndo(c *C) {
	s.testUpdateWithComponentsRunThroughOnlyComponentUpdate(c, updateWithComponentsOpts{
		snapType:              snap.TypeApp,
		instanceKey:           "key",
		components:            []string{"standard-component", "standard-component-extra"},
		refreshAppAwarenessUX: true,
		undo:                  true,
	})
}

func (s *snapmgrTestSuite) testUpdateWithComponentsRunThroughOnlyComponentUpdate(c *C, opts updateWithComponentsOpts) {
	model := MakeModel20("pc", map[string]any{"base": "core24"})
	r := snapstatetest.MockDeviceModel(model)
	defer r()

	if opts.postRefreshComponents != nil {
		c.Fatalf("when refreshing a snap that results in only component revision changes, you cannot lose or gain components")
	}

	if opts.refreshAppAwarenessUX {
		s.enableRefreshAppAwarenessUX()
	}

	var snapName, snapID string
	switch opts.snapType {
	case snap.TypeKernel:
		snapName = "kernel"
		snapID = "kernel-id"
	case snap.TypeApp:
		snapName = "some-snap"
		snapID = "some-snap-id"
	}

	bl := boottest.MockUC20RunBootenv(bootloadertest.Mock("mock", c.MkDir()))
	bl.SetBootVars(map[string]string{"snap_kernel": snapName, "snap_core": "core24_2.snap"})
	bootloader.Force(bl)
	defer bootloader.Force(nil)

	currentSnapRev := snap.R(7)
	kPlaceInfo, err := snap.ParsePlaceInfoFromSnapFileName("kernel_" + currentSnapRev.String() + ".snap")
	c.Assert(err, IsNil)
	restore := bl.SetEnabledKernel(kPlaceInfo)
	defer restore()

	channel := "channel-for-components-only-component-refresh"
	s.fakeStore.refreshRevnos = map[string]snap.Revision{
		snapID: currentSnapRev,
	}

	instanceName := snap.InstanceName(snapName, opts.instanceKey)

	sort.Strings(opts.components)

	originalCompRevisions := make(map[string]snap.Revision)
	for i, compName := range opts.components {
		originalCompRevisions[compName] = snap.R(i + 1)
	}

	updatedCompRevisions := make(map[string]snap.Revision)
	for i, compName := range opts.components {
		updatedCompRevisions[compName] = snap.R(i + 2)
	}

	s.fakeStore.snapResourcesFn = func(info *snap.Info) []store.SnapResourceResult {
		c.Assert(info.InstanceName(), DeepEquals, instanceName)
		var results []store.SnapResourceResult
		for _, compName := range opts.components {
			results = append(results, store.SnapResourceResult{
				DownloadInfo: snap.DownloadInfo{
					DownloadURL: "http://example.com/" + compName,
				},
				Name:      compName,
				Revision:  updatedCompRevisions[compName].N,
				Type:      fmt.Sprintf("component/%s", componentNameToType(c, compName)),
				Version:   "1.0",
				CreatedAt: "2024-01-01T00:00:00Z",
			})
		}
		return results
	}

	// we start without the auxiliary store info (or with an older one)
	c.Check(backend.AuxStoreInfoFilename(snapID), testutil.FileAbsent)

	si := snap.SideInfo{
		RealName: snapName,
		Revision: currentSnapRev,
		SnapID:   snapID,
		Channel:  channel,
	}

	snaptest.MockSnapInstance(c, instanceName,
		fmt.Sprintf("name: %s\ntype: %s\n", snapName, opts.snapType), &si)
	fi, err := os.Stat(snap.MountFile(instanceName, si.Revision))
	c.Assert(err, IsNil)

	refreshedDate := fi.ModTime()

	restore = snapstate.MockRevisionDate(nil)
	defer restore()

	now, err := time.Parse(time.RFC3339, "2021-06-10T10:00:00Z")
	c.Assert(err, IsNil)

	restore = snapstate.MockTimeNow(func() time.Time {
		return now
	})
	defer restore()

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

	if opts.instanceKey != "" {
		tr := config.NewTransaction(s.state)
		tr.Set("core", "experimental.parallel-instances", true)
		tr.Commit()
	}

	currentSeq := snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si})

	var currentResources map[string]snap.Revision
	for _, comp := range opts.components {
		err := currentSeq.AddComponentForRevision(currentSnapRev, &sequence.ComponentState{
			SideInfo: &snap.ComponentSideInfo{
				Component: naming.NewComponentRef(snapName, comp),
				Revision:  originalCompRevisions[comp],
			},
			CompType: componentNameToType(c, comp),
		})
		c.Assert(err, IsNil)

		if currentResources == nil {
			currentResources = make(map[string]snap.Revision, len(opts.components))
		}
		currentResources[comp] = originalCompRevisions[comp]
	}
	currentComponentStates := currentSeq.Revisions[0].Components

	expectedComponentStates := make([]*sequence.ComponentState, 0, len(opts.components))
	for _, comp := range opts.components {
		expectedComponentStates = append(expectedComponentStates, &sequence.ComponentState{
			SideInfo: &snap.ComponentSideInfo{
				Component: naming.NewComponentRef(snapName, comp),
				Revision:  updatedCompRevisions[comp],
			},
			CompType: componentNameToType(c, comp),
		})
	}

	s.AddCleanup(snapstate.MockReadComponentInfo(func(
		compMntDir string, info *snap.Info, csi *snap.ComponentSideInfo,
	) (*snap.ComponentInfo, error) {
		return &snap.ComponentInfo{
			Component:         csi.Component,
			Type:              componentNameToType(c, csi.Component.ComponentName),
			CompVersion:       "1.0",
			ComponentSideInfo: *csi,
		}, nil
	}))

	snapstate.Set(s.state, instanceName, &snapstate.SnapState{
		Active:          true,
		Sequence:        currentSeq,
		Current:         si.Revision,
		SnapType:        string(opts.snapType),
		TrackingChannel: channel,
		InstanceKey:     opts.instanceKey,
	})

	ts, err := snapstate.Update(s.state, instanceName, nil, s.user.ID, snapstate.Flags{})
	c.Assert(err, IsNil)

	chg := s.state.NewChange("refresh", "refresh a snap")
	chg.AddAll(ts)

	if opts.undo {
		last := lastWithLane(ts.Tasks())
		c.Assert(last, NotNil)

		terr := s.state.NewTask("error-trigger", "provoking total undo")
		terr.WaitFor(last)
		terr.JoinLane(last.Lanes()[0])
		chg.AddTask(terr)
	}

	// check unlink-reason
	unlinkTask := findLastTask(chg, "unlink-current-snap")
	c.Assert(unlinkTask, NotNil)
	var unlinkReason string
	unlinkTask.Get("unlink-reason", &unlinkReason)
	c.Check(unlinkReason, Equals, "refresh")

	// local modifications, edge must be set
	te := ts.MaybeEdge(snapstate.LastBeforeLocalModificationsEdge)
	c.Assert(te, NotNil)
	c.Assert(te.Kind(), Equals, "validate-component")

	// we manually settle here since this test can be slow when the host is
	// under load
	s.state.Unlock()
	err = s.o.Settle(testutil.HostScaledTimeout(15 * time.Second))
	s.state.Lock()
	c.Assert(err, IsNil, Commentf("change tasks:\n%s", printTasks(chg.Tasks())))

	if opts.undo {
		c.Assert(chg.Err(), NotNil, Commentf("change tasks:\n%s", printTasks(chg.Tasks())))
	} else {
		c.Assert(chg.Err(), IsNil, Commentf("change tasks:\n%s", printTasks(chg.Tasks())))
	}

	expected := fakeOps{
		{
			op: "storesvc-snap-action",
			curSnaps: []store.CurrentSnap{{
				InstanceName:    instanceName,
				SnapID:          snapID,
				Revision:        currentSnapRev,
				TrackingChannel: channel,
				RefreshedDate:   refreshedDate,
				Epoch:           snap.E("1*"),
				Resources:       currentResources,
			}},
			userID: 1,
		},
		{
			op: "storesvc-snap-action:action",
			action: store.SnapAction{
				Action:       "refresh",
				InstanceName: instanceName,
				SnapID:       snapID,
				Channel:      channel,
				Flags:        store.SnapActionEnforceValidation,
			},
			revno:  currentSnapRev,
			userID: 1,
		},
	}

	if !opts.refreshAppAwarenessUX {
		expected = append(expected, fakeOp{
			op:   "remove-snap-aliases",
			name: instanceName,
		})
	}

	for _, cs := range expectedComponentStates {
		compName := cs.SideInfo.Component.ComponentName
		compRev := cs.SideInfo.Revision
		containerName := fmt.Sprintf("%s+%s", instanceName, compName)
		filename := fmt.Sprintf("%s_%v.comp", containerName, compRev)

		expected = append(expected, []fakeOp{{
			op:   "storesvc-download",
			name: cs.SideInfo.Component.String(),
		}, {
			op:                "validate-component:Doing",
			name:              instanceName,
			revno:             currentSnapRev,
			componentName:     compName,
			componentPath:     filepath.Join(dirs.SnapBlobDir, filename),
			componentRev:      compRev,
			componentSideInfo: *cs.SideInfo,
		}}...)
	}

	for _, cs := range expectedComponentStates {
		compName := cs.SideInfo.Component.ComponentName
		compRev := cs.SideInfo.Revision
		containerName := fmt.Sprintf("%s+%s", instanceName, compName)
		filename := fmt.Sprintf("%s_%v.comp", containerName, compRev)

		expected = append(expected, []fakeOp{{
			op:                "setup-component",
			containerName:     containerName,
			containerFileName: filename,
		}, {
			op:   "unlink-component",
			path: snap.ComponentMountDir(compName, originalCompRevisions[compName], instanceName),
		}}...)
	}

	expected = append(expected,
		fakeOp{
			op:          "run-inhibit-snap-for-unlink",
			name:        instanceName,
			inhibitHint: "refresh",
		},
		fakeOp{
			op:   "discard-namespace-locked",
			name: instanceName,
		},
		fakeOp{
			op:                 "unlink-snap",
			path:               filepath.Join(dirs.SnapMountDir, instanceName, currentSnapRev.String()),
			unlinkSkipBinaries: opts.refreshAppAwarenessUX,
			inhibitHint:        "refresh",
		})
	if opts.snapType == snap.TypeKernel {
		expected = append(expected,
			fakeOp{
				op: "prepare-kernel-snap",
			},
			fakeOp{
				op:    "update-gadget-assets:Doing",
				name:  "kernel",
				revno: currentSnapRev,
			})
	}
	expected = append(expected, fakeOps{
		{
			op:   "copy-data",
			path: filepath.Join(dirs.SnapMountDir, instanceName, currentSnapRev.String()),
			old:  filepath.Join(dirs.SnapMountDir, instanceName, currentSnapRev.String()),
		},
		{
			op:   "setup-snap-save-data",
			path: filepath.Join(dirs.SnapDataSaveDir, instanceName),
		},
	}...)

	expected = append(expected, fakeOps{
		{
			op:    "setup-profiles:Doing",
			name:  instanceName,
			revno: currentSnapRev,
		},
		{
			op: "candidate",
			sinfo: snap.SideInfo{
				RealName: snapName,
				SnapID:   snapID,
				Channel:  channel,
				Revision: currentSnapRev,
			},
		},
		{
			op:                  "link-snap",
			path:                filepath.Join(dirs.SnapMountDir, instanceName, currentSnapRev.String()),
			requireSnapdTooling: true,
		},
	}...)

	var currentKmodComps = []*snap.ComponentSideInfo{}
	for _, cs := range currentComponentStates {
		if cs.CompType == snap.KernelModulesComponent {
			currentKmodComps = append(currentKmodComps, cs.SideInfo)
		}
	}

	var newKmodComps = []*snap.ComponentSideInfo{}
	for _, cs := range expectedComponentStates {
		if cs.CompType == snap.KernelModulesComponent {
			newKmodComps = append(newKmodComps, cs.SideInfo)
		}
	}

	if len(currentKmodComps) == 0 && len(newKmodComps) == 0 {
		expected = append(expected, fakeOp{
			op: "maybe-set-next-boot",
		})
	}

	for _, cs := range expectedComponentStates {
		compName := cs.SideInfo.Component.ComponentName
		compRev := cs.SideInfo.Revision
		expected = append(expected, []fakeOp{
			{
				op:   "link-component",
				path: snap.ComponentMountDir(compName, compRev, instanceName),
			},
		}...)
	}

	expected = append(expected, fakeOps{
		{
			op:    "auto-connect:Doing",
			name:  instanceName,
			revno: currentSnapRev,
		},
		{
			op: "update-aliases",
		},
	}...)

	if len(currentKmodComps) > 0 || len(newKmodComps) > 0 {
		expected = append(expected, fakeOp{
			op:           "prepare-kernel-modules-components",
			currentComps: currentKmodComps,
			finalComps:   newKmodComps,
		}, fakeOp{
			op: "maybe-set-next-boot",
		})
	}

	if opts.snapType == snap.TypeKernel {
		expected = append(expected, fakeOp{op: "remove-kernel-snap-setup"})
	}

	for _, cs := range currentComponentStates {
		compName := cs.SideInfo.Component.ComponentName
		compRev := cs.SideInfo.Revision
		containerName := fmt.Sprintf("%s+%s", instanceName, compName)
		removedFilename := fmt.Sprintf("%s_%v.comp", containerName, compRev)
		expected = append(expected, []fakeOp{
			{
				op:                "undo-setup-component",
				containerName:     containerName,
				containerFileName: removedFilename,
				componentRemoveOpts: backend.RemoveComponentOpts{
					MaybeInitramfsMounted: compName == "kernel-modules-component"},
			},
			{
				op:                "remove-component-dir",
				containerName:     containerName,
				containerFileName: removedFilename,
			},
		}...)
	}

	expectedSideState := sequence.NewRevisionSideState(&si, expectedComponentStates)
	originalSideState := currentSeq.Revisions[0]

	if opts.undo {
		expected = append(expected, undoOps(instanceName, opts.snapType, expectedSideState, originalSideState)...)
	} else {
		expected = append(expected, fakeOp{
			op:    "cleanup-trash",
			name:  instanceName,
			revno: currentSnapRev,
		})
	}

	downloads := make([]fakeDownload, 0, len(opts.components))
	for _, compName := range opts.components {
		downloads = append(downloads, fakeDownload{
			macaroon: s.user.StoreMacaroon,
			name:     fmt.Sprintf("%s+%s", snapName, compName),
			target:   filepath.Join(dirs.SnapBlobDir, fmt.Sprintf("%s+%s_%d.comp", instanceName, compName, updatedCompRevisions[compName].N)),
		})
	}
	c.Check(s.fakeStore.downloads, DeepEquals, downloads)

	c.Check(s.fakeStore.seenPrivacyKeys["privacy-key"], Equals, true, Commentf("salts seen: %v", s.fakeStore.seenPrivacyKeys))

	// start with an easier-to-read error if this fails:
	c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
	c.Assert(s.fakeBackend.ops, DeepEquals, expected)

	task := ts.Tasks()[1]

	// verify snapSetup info
	var snapsup snapstate.SnapSetup
	err = task.Get("snap-setup", &snapsup)
	c.Assert(err, IsNil)
	c.Assert(snapsup.DownloadInfo, DeepEquals, &snap.DownloadInfo{
		DownloadURL: "https://some-server.com/some/path.snap",
		Sha3_384:    "<some-hash>",
	})
	c.Assert(snapsup, DeepEquals, snapstate.SnapSetup{
		Channel: channel,
		UserID:  s.user.ID,
		DownloadInfo: &snap.DownloadInfo{
			DownloadURL: "https://some-server.com/some/path.snap",
			Sha3_384:    "<some-hash>",
		},
		SideInfo:  snapsup.SideInfo,
		Type:      opts.snapType,
		Version:   snapName + "Ver",
		PlugsOnly: true,
		Flags: snapstate.Flags{
			Transaction: client.TransactionPerSnap,
		},
		InstanceKey:                     opts.instanceKey,
		PreUpdateKernelModuleComponents: currentKmodComps,
	})
	c.Assert(snapsup.SideInfo, DeepEquals, &snap.SideInfo{
		RealName: snapName,
		Revision: currentSnapRev,
		Channel:  channel,
		SnapID:   snapID,
	})

	// verify snaps in the system state
	var snapst snapstate.SnapState
	err = snapstate.Get(s.state, instanceName, &snapst)
	c.Assert(err, IsNil)

	if !opts.undo {
		c.Assert(snapst.LastRefreshTime, NotNil)
		c.Check(snapst.LastRefreshTime.Equal(now), Equals, true)
		c.Assert(snapst.Active, Equals, true)

		// no new revision added to the sequence, the components should have
		// been replaced in-place
		c.Assert(snapst.Sequence.Revisions, HasLen, 1)
		c.Assert(snapst.Sequence.Revisions[0], DeepEquals, expectedSideState)
	} else {
		// make sure everything is back to how it started
		c.Assert(snapst.Active, Equals, true)
		c.Assert(snapst.Sequence.Revisions, HasLen, 1)
		c.Assert(snapst.Sequence.Revisions[0].Snap, DeepEquals, currentSeq.Revisions[0].Snap)

		// TODO: figure out why this is out of order and if it is a problem
		c.Assert(snapst.Sequence.Revisions[0].Components, testutil.DeepUnsortedMatches, currentSeq.Revisions[0].Components)
	}
}

func checkComponentSetupTasks(c *C, ts *state.TaskSet, expected []snapstate.ComponentSetup, taskKind string) {
	found := make([]snapstate.ComponentSetup, 0, len(expected))
	for _, t := range ts.Tasks() {
		if !t.Has("component-setup") {
			continue
		}

		if t.Kind() != taskKind {
			c.Errorf("component-setup found on unexpected task kind %q", t.Kind())
		}

		var compsup snapstate.ComponentSetup
		err := t.Get("component-setup", &compsup)
		c.Assert(err, IsNil)

		found = append(found, compsup)
	}

	c.Assert(found, HasLen, len(expected))
	c.Check(found, testutil.DeepUnsortedMatches, expected)
}

func (s *snapmgrTestSuite) TestBlockUnlinkAffectingHook(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	hooksup := &hookstate.HookSetup{Snap: "some-snap"}
	hook := hookstate.HookTask(s.state, "blocking hook", hooksup, nil)
	si := &snap.SideInfo{
		RealName: "some-snap",
		SnapID:   "some-snap-id",
		Revision: snap.R(1),
	}
	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
		Active:   true,
		Current:  si.Revision,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}),
		Base:     "some-base",
	})

	type testcase struct {
		task          string
		blockStatuses []state.Status
	}

	tcs := []testcase{
		{
			task:          "unlink-current-snap",
			blockStatuses: []state.Status{state.DoingStatus, state.DoStatus},
		},

		{
			task:          "link-snap",
			blockStatuses: []state.Status{state.UndoStatus, state.UndoingStatus},
		},
		{
			task:          "unlink-snap",
			blockStatuses: []state.Status{state.DoStatus, state.DoingStatus},
		},
	}

	for _, tc := range tcs {
		for _, status := range []state.Status{state.DoStatus, state.DoingStatus, state.UndoStatus, state.UndoingStatus} {
			var expectBlocked bool
			for _, s := range tc.blockStatuses {
				if status == s {
					expectBlocked = true
					break
				}
			}

			task := s.state.NewTask(tc.task, "summary")
			task.SetStatus(status)
			task.Set("snap-setup", &snapstate.SnapSetup{
				SideInfo: &snap.SideInfo{
					RealName: "some-snap",
					SnapID:   "some-snap-id",
					Revision: snap.R(1),
				},
			})

			// unlinking a snap with running hook
			block := snapstate.AffectsRunningHooks(task, []*state.Task{hook})
			c.Assert(block, Equals, expectBlocked)

			task.Set("snap-setup", &snapstate.SnapSetup{
				SideInfo: &snap.SideInfo{
					RealName: "some-base",
					SnapID:   "some-base-id",
					Revision: snap.R(1),
				},
			})

			// unlinking the base of a snap with running hook
			block = snapstate.AffectsRunningHooks(task, []*state.Task{hook})
			c.Assert(block, Equals, expectBlocked)

			// unlinking a snap unrelated to the hook
			task.Set("snap-setup", &snapstate.SnapSetup{
				SideInfo: &snap.SideInfo{
					RealName: "other-snap",
					SnapID:   "other-snap-id",
					Revision: snap.R(1),
				},
			})
			block = snapstate.AffectsRunningHooks(task, []*state.Task{hook})
			c.Assert(block, Equals, false)
		}
	}
}

const (
	v1SnapYaml = `name: test-snap
version: 1.0
apps:
 normal-app:
  command: bin/test
 foo-svc:
  command: bin/service
  daemon: simple
 bar-svc:
  command: bin/service
  daemon: simple
 baz-svc:
  command: bin/user-service
  daemon: simple
`

	v2SnapYaml = `name: test-snap
version: 2.0
apps:
 normal-app:
  command: bin/test
 foo-svc:
  command: bin/service
  daemon: simple
 bar-svc:
  command: bin/service
`
)

func (s *snapmgrTestSuite) TestStopSnapServicesComputesRemovedServices(c *C) {
	s.state.Lock()
	defer s.state.Unlock()

	// setup the installed revision
	oldSi := &snap.SideInfo{RealName: "test-snap", Revision: snap.R(11)}
	oldInfo := snaptest.MockSnap(c, v1SnapYaml, oldSi)
	snapstate.Set(s.state, "test-snap", &snapstate.SnapState{
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{oldSi}),
		Current:  snap.R(11),
		Active:   true,
	})

	// refreshing to revision with different set of services
	newSi := &snap.SideInfo{RealName: "test-snap", Revision: snap.R(12)}
	newInfo := snaptest.MockSnap(c, v2SnapYaml, newSi)
	snapsup := &snapstate.SnapSetup{
		SideInfo: &snap.SideInfo{
			RealName: "test-snap",
			Revision: snap.R(12),
			SnapID:   "test-snap-id",
		},
	}

	s.fakeBackend.infos = map[string]map[snap.Revision]*snap.Info{
		"test-snap": {
			oldSi.Revision: oldInfo,
			newSi.Revision: newInfo,
		},
	}

	chg := s.state.NewChange("stop-services", "stop the services")
	task := s.state.NewTask("stop-snap-services", "Stop snap services")
	task.Set("stop-reason", snap.StopReasonRefresh)
	task.Set("snap-setup", snapsup)
	chg.AddTask(task)

	s.settle(c)

	c.Assert(chg.Err(), IsNil)
	c.Assert(chg.IsReady(), Equals, true)

	c.Assert(s.fakeBackend.ops, DeepEquals, fakeOps{
		{
			op: "current-snap-service-states",
		},
		{
			op:       "stop-snap-services:refresh",
			path:     oldInfo.MountDir(),
			services: []string{"bar-svc", "baz-svc", "foo-svc"},
			// baz-svc is removed and bar-svc is turned into a "normal" app
			removedServices: []string{"bar-svc", "baz-svc"},
		},
	})
}

func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefresh(c *C) {
	restore := snapstate.MockRevisionDate(nil)
	defer restore()
	observed, restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{
		"kernel": "kernel",
		"base":   "core18",
	}, []string{"kernel", "core18"})
	defer restore()

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

	s.installSeedRefreshSnaps(c,
		seedRefreshSnap{name: "kernel", snapID: "kernel-id", snapType: "kernel"},
		seedRefreshSnap{name: "core18", snapID: "core18-snap-id", snapType: "base"},
		seedRefreshSnap{name: "some-app", snapID: "some-app-id", snapType: "app", base: "core18"},
	)

	uts, _ := runSeedRefreshUpdate(c, s.state, s.user.ID, []snapstate.StoreUpdate{
		{InstanceName: "kernel"},
		{InstanceName: "core18"},
		{InstanceName: "some-app"},
	})
	taskSetsBySnap, seedTS := parseSeedRefreshTaskSets(uts)

	baseTS := mustTaskSetForSnap(c, taskSetsBySnap, "core18")
	kernelTS := mustTaskSetForSnap(c, taskSetsBySnap, "kernel")
	appTS := mustTaskSetForSnap(c, taskSetsBySnap, "some-app")
	c.Assert(observed.initial, HasLen, 1)
	c.Check(observed.initial[0], testutil.DeepUnsortedMatches, []snapstate.SeedRefreshCandidate{
		seedRefreshCandidateFromTaskSet(c, kernelTS),
		seedRefreshCandidateFromTaskSet(c, baseTS),
		seedRefreshCandidateFromTaskSet(c, appTS),
	})
	c.Check(observed.evictions, DeepEquals, []snapstate.SeedRefreshEvictionPolicy{{SeedsToRetain: 1}})

	c.Assert(seedTS, NotNil)
	c.Check(taskSetsShareLane(baseTS, kernelTS), Equals, true)
	c.Check(taskSetsShareLane(baseTS, appTS), Equals, false)
	c.Check(taskSetLanes(seedTS), testutil.DeepUnsortedMatches, taskSetLanes(baseTS))

	seedCreate, seedEnd, _ := splitSeedRefreshTasks(c, seedTS)

	lastBeforeLocalBase, err := baseTS.Edge(snapstate.LastBeforeLocalModificationsEdge)
	c.Assert(err, IsNil)
	lastBeforeLocalKernel, err := kernelTS.Edge(snapstate.LastBeforeLocalModificationsEdge)
	c.Assert(err, IsNil)

	for _, t := range []*state.Task{lastBeforeLocalBase, lastBeforeLocalKernel} {
		c.Check(waitsOnTransitively(seedCreate, t), Equals, true)
	}

	kernelLinkTask, err := kernelTS.Edge(snapstate.MaybeRebootEdge)
	c.Assert(err, IsNil)
	c.Check(kernelLinkTask.HaltTasks(), testutil.Contains, seedCreate)

	for _, ts := range []*state.TaskSet{baseTS, kernelTS} {
		end, err := ts.Edge(snapstate.EndEdge)
		c.Assert(err, IsNil)
		c.Check(waitsOnTransitively(seedEnd, end), Equals, true)
	}

	c.Check(hasDoRestartBoundary(seedCreate), Equals, true)
	c.Check(hasDoRestartBoundary(kernelLinkTask), Equals, false)
}

func (s *snapmgrTestSuite) testUpdateWithGoalSeedRefreshBeforeLocalModificationsModelSnap(c *C, classic bool) {
	restore := snapstate.MockRevisionDate(nil)
	defer restore()
	observed, restore := s.setupSeedRefreshUpdateTest(c, classic, true, map[string]any{
		"kernel":         "kernel",
		"base":           "core18",
		"required-snaps": []any{"some-app"},
	}, []string{"kernel", "core18", "some-app"})
	defer restore()

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

	s.installSeedRefreshSnaps(c,
		seedRefreshSnap{name: "kernel", snapID: "kernel-id", snapType: "kernel"},
		seedRefreshSnap{name: "core18", snapID: "core18-snap-id", snapType: "base"},
		seedRefreshSnap{name: "some-app", snapID: "some-app-id", snapType: "app", base: "core18"},
		seedRefreshSnap{name: "some-other-snap", snapID: "some-other-snap-id", snapType: "app", base: "core18"},
	)

	uts, chg := runSeedRefreshUpdate(c, s.state, s.user.ID, []snapstate.StoreUpdate{
		{InstanceName: "kernel"},
		{InstanceName: "core18"},
		{InstanceName: "some-app"},
		{InstanceName: "some-other-snap"},
	})
	taskSetsBySnap, seedTS := parseSeedRefreshTaskSets(uts)

	baseTS := mustTaskSetForSnap(c, taskSetsBySnap, "core18")
	kernelTS := mustTaskSetForSnap(c, taskSetsBySnap, "kernel")
	appTS := mustTaskSetForSnap(c, taskSetsBySnap, "some-app")
	nonSeedAppTS := mustTaskSetForSnap(c, taskSetsBySnap, "some-other-snap")
	c.Assert(observed.initial, HasLen, 1)
	c.Check(observed.initial[0], testutil.DeepUnsortedMatches, []snapstate.SeedRefreshCandidate{
		seedRefreshCandidateFromTaskSet(c, kernelTS),
		seedRefreshCandidateFromTaskSet(c, baseTS),
		seedRefreshCandidateFromTaskSet(c, appTS),
		seedRefreshCandidateFromTaskSet(c, nonSeedAppTS),
	})
	c.Check(observed.evictions, DeepEquals, []snapstate.SeedRefreshEvictionPolicy{{SeedsToRetain: 1}})

	c.Assert(seedTS, NotNil)
	c.Check(taskSetsShareLane(baseTS, kernelTS, appTS), Equals, true)
	c.Check(taskSetsShareLane(baseTS, nonSeedAppTS), Equals, false)
	c.Check(taskSetLanes(seedTS), testutil.DeepUnsortedMatches, taskSetLanes(baseTS))

	seedCreate, seedEnd, _ := splitSeedRefreshTasks(c, seedTS)

	kernelLinkTask, err := kernelTS.Edge(snapstate.MaybeRebootEdge)
	c.Assert(err, IsNil)
	c.Check(kernelLinkTask.HaltTasks(), testutil.Contains, seedCreate)

	for _, ts := range []*state.TaskSet{baseTS, kernelTS, appTS} {
		end, err := ts.Edge(snapstate.EndEdge)
		c.Assert(err, IsNil)
		c.Check(waitsOnTransitively(seedEnd, end), Equals, true)
	}

	c.Check(hasDoRestartBoundary(seedCreate), Equals, true)
	c.Check(hasDoRestartBoundary(kernelLinkTask), Equals, false)

	lastEssentialSnapTask, err := kernelTS.Edge(snapstate.EndEdge)
	c.Assert(err, IsNil)
	nonSeedAppFirstPrereq, _ := findPrereqTasksForSnap(c, chg, "some-other-snap")
	lastBeforeLocalModelApp, err := appTS.Edge(snapstate.LastBeforeLocalModificationsEdge)
	c.Assert(err, IsNil)
	lastBeforeLocalNonSeedApp, err := nonSeedAppTS.Edge(snapstate.LastBeforeLocalModificationsEdge)
	c.Assert(err, IsNil)
	c.Check(waitsOnTransitively(seedCreate, lastBeforeLocalModelApp), Equals, true)
	c.Check(waitsOnTransitively(seedCreate, nonSeedAppFirstPrereq), Equals, true)
	c.Check(waitsOnTransitively(seedCreate, lastBeforeLocalNonSeedApp), Equals, false)

	for _, lane := range seedCreate.Lanes() {
		c.Check(nonSeedAppFirstPrereq.Lanes(), testutil.Contains, lane)
		c.Check(lastBeforeLocalNonSeedApp.Lanes(), Not(testutil.Contains), lane)
	}

	// model app performs before-local-modification tasks before seed creation, but
	// doesn't perform any local modifications until all essential snaps are complete.
	firstLocalModApp := firstTaskAfterLocalModifications(c, appTS)
	c.Check(firstLocalModApp.WaitTasks(), testutil.Contains, lastEssentialSnapTask)

	// non-seed app performs its initial prerequisite check before seed creation,
	// but defers post-prerequisite work until all essential snaps are complete.
	firstPostPrereqsNonSeedApp := firstTaskAfterPrerequisites(c, nonSeedAppTS)
	c.Check(firstPostPrereqsNonSeedApp.WaitTasks(), testutil.Contains, lastEssentialSnapTask)

	// ensure that seed finalize task doesn't wait on non-seed snap
	nonSeedAppEndTask, err := nonSeedAppTS.Edge(snapstate.EndEdge)
	c.Assert(err, IsNil)
	c.Check(waitsOnTransitively(seedEnd, nonSeedAppEndTask), Equals, false)
}

func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshBeforeLocalModificationsModelSnap(c *C) {
	const classic = false
	s.testUpdateWithGoalSeedRefreshBeforeLocalModificationsModelSnap(c, classic)
}

func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshBeforeLocalModificationsModelSnapOnClassic(c *C) {
	// TODO: this variant will need to be more complex once seed-refresh mode
	// does not disable split refresh.
	const classic = true
	s.testUpdateWithGoalSeedRefreshBeforeLocalModificationsModelSnap(c, classic)
}

func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshBaseAndModelSnapRun(c *C) {
	_, restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{
		"kernel":         "kernel",
		"base":           "core18",
		"required-snaps": []any{"some-snap-with-core18-base"},
	}, []string{"core18", "some-snap-with-core18-base"})
	defer restore()

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

	mockSeedRefreshRebootHandlers(s, c, nil)

	s.installSeedRefreshSnaps(c,
		seedRefreshSnap{name: "kernel", snapID: "kernel-id", snapType: "kernel"},
		seedRefreshSnap{name: "core18", snapID: "core18-snap-id", snapType: "base"},
		seedRefreshSnap{name: "some-snap-with-core18-base", snapID: "some-snap-with-core18-base", snapType: "app", base: "core18"},
	)

	uts, chg := runSeedRefreshUpdate(c, s.state, s.user.ID, []snapstate.StoreUpdate{
		{InstanceName: "core18"},
		{InstanceName: "some-snap-with-core18-base"},
	})

	taskSetsBySnap, seedTS := parseSeedRefreshTaskSets(uts)
	appTS := mustTaskSetForSnap(c, taskSetsBySnap, "some-snap-with-core18-base")
	c.Assert(seedTS, NotNil)

	appSnapSetupTask, err := appTS.Edge(snapstate.SnapSetupEdge)
	c.Assert(err, IsNil)
	appSnapSetup, err := snapstate.TaskSnapSetup(appSnapSetupTask)
	c.Assert(err, IsNil)
	c.Check(appSnapSetup.Base, Equals, "core18")

	s.settle(c)
	s.mockRestartAndSettle(c, chg)

	c.Assert(chg.Err(), IsNil)
	c.Assert(chg.IsReady(), Equals, true)
}

func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshPrerequisitesUpdatesModelSnap(c *C) {
	observed, restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{
		"kernel":         "kernel",
		"base":           "core18",
		"required-snaps": []any{"content-provider", "some-app"},
	}, []string{"content-provider", "some-app"})
	defer restore()

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

	ifacerepo.Replace(s.state, interfaces.NewRepository())
	s.fakeStore.mutateSnapInfo = func(info *snap.Info) error {
		if info.InstanceName() != "some-app" {
			return nil
		}

		info.Plugs = map[string]*snap.PlugInfo{
			"shared-content": {
				Snap:      info,
				Name:      "shared-content",
				Interface: "content",
				Attrs: map[string]any{
					"default-provider": "content-provider",
					"content":          "shared-content",
				},
			},
		}

		return nil
	}

	mockSeedRefreshRebootHandlers(s, c, nil)

	s.installSeedRefreshSnaps(c,
		seedRefreshSnap{name: "kernel", snapID: "kernel-id", snapType: "kernel"},
		seedRefreshSnap{name: "core18", snapID: "core18-snap-id", snapType: "base"},
		seedRefreshSnap{name: "content-provider", snapID: "content-provider-id", snapType: "app", base: "core18"},
		seedRefreshSnap{name: "some-app", snapID: "some-app-id", snapType: "app", base: "core18"},
	)

	uts, chg := runSeedRefreshUpdate(c, s.state, s.user.ID, []snapstate.StoreUpdate{{InstanceName: "some-app"}})
	taskSetsBySnap, seedTS := parseSeedRefreshTaskSets(uts)
	appTS := mustTaskSetForSnap(c, taskSetsBySnap, "some-app")
	c.Assert(seedTS, NotNil)

	appSnapSetupTask, err := appTS.Edge(snapstate.SnapSetupEdge)
	c.Assert(err, IsNil)

	appSnapSetup, err := snapstate.TaskSnapSetup(appSnapSetupTask)
	c.Assert(err, IsNil)
	c.Assert(appSnapSetup.PrereqContentAttrs, DeepEquals, map[string][]string{"content-provider": {"shared-content"}})

	s.settle(c)

	seedCreate, seedFinalize, _ := splitSeedRefreshTasks(c, seedTS)
	c.Assert(observed.prerequisites, HasLen, 1)
	c.Assert(observed.prerequisites[0].SnapSetupTaskIDs, HasLen, 1)

	providerSnapSetupTask := s.state.Task(observed.prerequisites[0].SnapSetupTaskIDs[0])
	c.Assert(providerSnapSetupTask, NotNil)

	providerSnapSetup, err := snapstate.TaskSnapSetup(providerSnapSetupTask)
	c.Assert(err, IsNil)
	c.Check(providerSnapSetup.InstanceName(), Equals, "content-provider")
	c.Check(observed.prerequisites, testutil.DeepUnsortedMatches, []snapstate.SeedRefreshCandidate{{
		InstanceName:     providerSnapSetup.InstanceName(),
		SnapSetupTaskIDs: []string{providerSnapSetupTask.ID()},
	}})

	// ensure create-recovery-system waits on the LastBeforeLocalModificationsEdge for content-provider
	c.Check(waitTasksContainKindForSnap(c, seedCreate, "content-provider", "validate-snap"), Equals, true)
	// ensure finalize-recovery-system waits on the EndEdge for content-provider
	c.Check(waitTasksContainKindForSnap(c, seedFinalize, "content-provider", "run-hook"), Equals, true)

	s.mockRestartAndSettle(c, chg)

	c.Assert(chg.Err(), IsNil)
	c.Assert(chg.IsReady(), Equals, true)
}

func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshPrerequisitesDoNotMergeWhenSeedRefreshAlreadyReady(c *C) {
	observed, restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{
		"kernel":         "kernel",
		"base":           "core18",
		"required-snaps": []any{"content-provider", "some-app"},
	}, []string{"content-provider", "some-app"})
	defer restore()

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

	ifacerepo.Replace(s.state, interfaces.NewRepository())
	s.fakeStore.mutateSnapInfo = func(info *snap.Info) error {
		if info.InstanceName() != "some-app" {
			return nil
		}

		info.Plugs = map[string]*snap.PlugInfo{
			"shared-content": {
				Snap:      info,
				Name:      "shared-content",
				Interface: "content",
				Attrs: map[string]any{
					"default-provider": "content-provider",
					"content":          "shared-content",
				},
			},
		}

		return nil
	}

	mockSeedRefreshRebootHandlers(s, c, nil)

	s.installSeedRefreshSnaps(c,
		seedRefreshSnap{name: "kernel", snapID: "kernel-id", snapType: "kernel"},
		seedRefreshSnap{name: "core18", snapID: "core18-snap-id", snapType: "base"},
		seedRefreshSnap{name: "content-provider", snapID: "content-provider-id", snapType: "app", base: "core18"},
		seedRefreshSnap{name: "some-app", snapID: "some-app-id", snapType: "app", base: "core18"},
	)

	uts, chg := runSeedRefreshUpdate(c, s.state, s.user.ID, []snapstate.StoreUpdate{{InstanceName: "some-app"}})
	_, seedTS := parseSeedRefreshTaskSets(uts)

	seedCreate, seedFinalize, _ := splitSeedRefreshTasks(c, seedTS)

	// manually mark these as done to prove that prerequisites won't add a snap
	// to an already completed seed-refresh
	seedCreate.SetStatus(state.DoneStatus)
	seedFinalize.SetStatus(state.DoneStatus)

	s.settle(c)

	// ensure that the seed-refresh tasks didn't get dependencies on the
	// content-provider
	c.Check(waitTasksContainKindForSnap(c, seedCreate, "content-provider", "validate-snap"), Equals, false)
	c.Check(waitTasksContainKindForSnap(c, seedFinalize, "content-provider", "run-hook"), Equals, false)
	c.Check(observed.prerequisites, HasLen, 0)

	c.Check(restart.Pending(s.state), Equals, restart.RestartSystem)

	// since content-provider is part of the model, we should end up creating
	// new recovery-system tasks, rather than updating the existing one
	c.Check(countTasksOfKind(chg.Tasks(), "create-recovery-system"), Equals, 2)
	c.Check(countTasksOfKind(chg.Tasks(), "finalize-recovery-system"), Equals, 2)
	c.Assert(chg.Err(), IsNil)
	c.Assert(chg.IsReady(), Equals, false)
}

// TODO:SEEDREFRESH: update this test once this scenario is supported
func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshPrerequisitesFailsForProviderSwitchingToInFlightNonEssentialBase(c *C) {
	_, restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{
		"kernel":         "kernel",
		"base":           "core18",
		"required-snaps": []any{"content-provider", "some-app"},
	}, []string{"content-provider", "some-app"})
	defer restore()

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

	ifacerepo.Replace(s.state, interfaces.NewRepository())
	s.fakeStore.mutateSnapInfo = func(info *snap.Info) error {
		switch info.InstanceName() {
		case "some-app":
			info.Plugs = map[string]*snap.PlugInfo{
				"shared-content": {
					Snap:      info,
					Name:      "shared-content",
					Interface: "content",
					Attrs: map[string]any{
						"default-provider": "content-provider",
						"content":          "shared-content",
					},
				},
			}
		case "content-provider":
			info.Base = "some-base"
		}

		return nil
	}

	s.installSeedRefreshSnaps(c,
		seedRefreshSnap{name: "kernel", snapID: "kernel-id", snapType: "kernel"},
		seedRefreshSnap{name: "core18", snapID: "core18-snap-id", snapType: "base"},
		seedRefreshSnap{name: "some-base", snapID: "some-base-id", snapType: "base"},
		seedRefreshSnap{name: "content-provider", snapID: "content-provider-id", snapType: "app", base: "core18"},
		seedRefreshSnap{name: "some-app", snapID: "some-app-id", snapType: "app", base: "core18"},
	)

	uts, _ := runSeedRefreshUpdate(c, s.state, s.user.ID, []snapstate.StoreUpdate{{InstanceName: "some-app"}, {InstanceName: "some-base"}})
	taskSetsBySnap, _ := parseSeedRefreshTaskSets(uts)
	appTS := mustTaskSetForSnap(c, taskSetsBySnap, "some-app")

	prereqs := findKindInTaskSet(appTS, "prerequisites")
	c.Assert(prereqs, NotNil)

	s.state.Unlock()
	err := s.o.SettleWithBreakCondition(testutil.HostScaledTimeout(10*time.Second), func() bool {
		s.state.Lock()
		defer s.state.Unlock()
		return prereqs.Status().Ready()
	})
	s.state.Lock()
	c.Assert(err, IsNil)

	c.Assert(prereqs.Status(), Equals, state.ErrorStatus)

	c.Assert(prereqs.Status(), Equals, state.ErrorStatus)
	c.Assert(strings.Join(prereqs.Log(), "\n"), Matches, `(?s).*cannot install prerequisite "content-provider": cannot automatically update prerequisite "content-provider" during seed-refresh while base "some-base" waits for create-recovery-system.*`)
}

func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshAllowsRequestedModelContentProviderRefresh(c *C) {
	_, restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{
		"kernel":         "kernel",
		"base":           "core18",
		"required-snaps": []any{"content-provider", "some-app"},
	}, []string{"content-provider", "some-app"})
	defer restore()

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

	ifacerepo.Replace(s.state, interfaces.NewRepository())
	s.fakeStore.mutateSnapInfo = func(info *snap.Info) error {
		if info.InstanceName() != "some-app" {
			return nil
		}

		info.Plugs = map[string]*snap.PlugInfo{
			"shared-content": {
				Snap:      info,
				Name:      "shared-content",
				Interface: "content",
				Attrs: map[string]any{
					"default-provider": "content-provider",
					"content":          "shared-content",
				},
			},
		}

		return nil
	}

	mockSeedRefreshRebootHandlers(s, c, nil)

	s.installSeedRefreshSnaps(c,
		seedRefreshSnap{name: "kernel", snapID: "kernel-id", snapType: "kernel"},
		seedRefreshSnap{name: "core18", snapID: "core18-snap-id", snapType: "base"},
		seedRefreshSnap{name: "content-provider", snapID: "content-provider-id", snapType: "app", base: "core18"},
		seedRefreshSnap{name: "some-app", snapID: "some-app-id", snapType: "app", base: "core18"},
	)

	uts, chg := runSeedRefreshUpdate(c, s.state, s.user.ID, []snapstate.StoreUpdate{
		{InstanceName: "content-provider"},
		{InstanceName: "some-app"},
	})
	taskSetsBySnap, seedTS := parseSeedRefreshTaskSets(uts)
	providerTS := mustTaskSetForSnap(c, taskSetsBySnap, "content-provider")
	appTS := mustTaskSetForSnap(c, taskSetsBySnap, "some-app")
	c.Assert(seedTS, NotNil)
	c.Check(taskSetsShareLane(providerTS, appTS), Equals, true)

	appSnapSetupTask, err := appTS.Edge(snapstate.SnapSetupEdge)
	c.Assert(err, IsNil)
	appSnapSetup, err := snapstate.TaskSnapSetup(appSnapSetupTask)
	c.Assert(err, IsNil)
	c.Assert(appSnapSetup.PrereqContentAttrs, DeepEquals, map[string][]string{"content-provider": {"shared-content"}})

	s.settle(c)
	s.mockRestartAndSettle(c, chg)

	c.Assert(chg.Err(), IsNil)
	c.Assert(chg.IsReady(), Equals, true)
}

func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshBeforeLocalModificationsWithSnapd(c *C) {
	restore := snapstate.MockRevisionDate(nil)
	defer restore()
	observed, restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{
		"kernel":         "kernel",
		"base":           "core18",
		"required-snaps": []any{"some-app"},
	}, []string{"snapd", "kernel", "core18", "some-app"})
	defer restore()

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

	s.installSeedRefreshSnaps(c,
		seedRefreshSnap{name: "snapd", snapID: "snapd-snap-id", snapType: "snapd"},
		seedRefreshSnap{name: "kernel", snapID: "kernel-id", snapType: "kernel"},
		seedRefreshSnap{name: "core18", snapID: "core18-snap-id", snapType: "base"},
		seedRefreshSnap{name: "some-app", snapID: "some-app-id", snapType: "app", base: "core18"},
		seedRefreshSnap{name: "some-other-snap", snapID: "some-other-snap-id", snapType: "app", base: "core18"},
	)

	uts, chg := runSeedRefreshUpdate(c, s.state, s.user.ID, []snapstate.StoreUpdate{
		{InstanceName: "snapd"},
		{InstanceName: "kernel"},
		{InstanceName: "core18"},
		{InstanceName: "some-app"},
		{InstanceName: "some-other-snap"},
	})
	taskSetsBySnap, seedTS := parseSeedRefreshTaskSets(uts)

	snapdTS := mustTaskSetForSnap(c, taskSetsBySnap, "snapd")
	baseTS := mustTaskSetForSnap(c, taskSetsBySnap, "core18")
	kernelTS := mustTaskSetForSnap(c, taskSetsBySnap, "kernel")
	appTS := mustTaskSetForSnap(c, taskSetsBySnap, "some-app")
	nonSeedAppTS := mustTaskSetForSnap(c, taskSetsBySnap, "some-other-snap")

	c.Assert(observed.initial, HasLen, 1)
	c.Check(observed.initial[0], testutil.DeepUnsortedMatches, []snapstate.SeedRefreshCandidate{
		seedRefreshCandidateFromTaskSet(c, snapdTS),
		seedRefreshCandidateFromTaskSet(c, kernelTS),
		seedRefreshCandidateFromTaskSet(c, baseTS),
		seedRefreshCandidateFromTaskSet(c, appTS),
		seedRefreshCandidateFromTaskSet(c, nonSeedAppTS),
	})

	c.Assert(seedTS, NotNil)
	c.Check(taskSetsShareLane(snapdTS, baseTS, kernelTS, appTS), Equals, true)
	c.Check(taskSetsShareLane(baseTS, nonSeedAppTS), Equals, false)
	c.Check(taskSetLanes(seedTS), testutil.DeepUnsortedMatches, taskSetLanes(snapdTS))

	seedCreate, seedEnd, _ := splitSeedRefreshTasks(c, seedTS)

	kernelLinkTask, err := kernelTS.Edge(snapstate.MaybeRebootEdge)
	c.Assert(err, IsNil)
	c.Check(kernelLinkTask.HaltTasks(), testutil.Contains, seedCreate)

	for _, ts := range []*state.TaskSet{snapdTS, baseTS, kernelTS, appTS} {
		end, err := ts.Edge(snapstate.EndEdge)
		c.Assert(err, IsNil)
		c.Check(waitsOnTransitively(seedEnd, end), Equals, true)
	}

	c.Check(hasDoRestartBoundary(seedCreate), Equals, true)
	c.Check(hasDoRestartBoundary(kernelLinkTask), Equals, false)

	snapdEnd, err := snapdTS.Edge(snapstate.EndEdge)
	c.Assert(err, IsNil)
	baseBegin, err := baseTS.Edge(snapstate.BeginEdge)
	c.Assert(err, IsNil)

	c.Check(baseBegin.WaitTasks(), testutil.Contains, snapdEnd)

	snapdLastBefore, err := snapdTS.Edge(snapstate.LastBeforeLocalModificationsEdge)
	c.Assert(err, IsNil)
	baseLastBefore, err := baseTS.Edge(snapstate.LastBeforeLocalModificationsEdge)
	c.Assert(err, IsNil)

	// assert seed creation waits on before-local-modification tasks of seed snaps.
	for _, lastBeforeLocal := range []*state.Task{snapdLastBefore, baseLastBefore} {
		c.Check(waitsOnTransitively(seedCreate, lastBeforeLocal), Equals, true)
	}

	nonSeedAppFirstPrereq, _ := findPrereqTasksForSnap(c, chg, "some-other-snap")
	nonSeedAppLastBefore, err := nonSeedAppTS.Edge(snapstate.LastBeforeLocalModificationsEdge)
	c.Assert(err, IsNil)

	// assert seed creation waits only on initial prerequisites tasks of non-seed snaps.
	c.Check(waitsOnTransitively(seedCreate, nonSeedAppFirstPrereq), Equals, true)
	c.Check(waitsOnTransitively(seedCreate, nonSeedAppLastBefore), Equals, false)
	for _, lane := range seedCreate.Lanes() {
		c.Check(nonSeedAppFirstPrereq.Lanes(), testutil.Contains, lane)
		c.Check(nonSeedAppLastBefore.Lanes(), Not(testutil.Contains), lane)
	}

	baseMount := findKindInTaskSet(baseTS, "mount-snap")
	c.Assert(baseMount, NotNil)
	c.Check(waitsOnTransitively(baseMount, snapdEnd), Equals, true)
	c.Check(waitsOnTransitively(baseMount, snapdLastBefore), Equals, true)

	lastEssentialSnapTask, err := kernelTS.Edge(snapstate.EndEdge)
	c.Assert(err, IsNil)

	// model app performs before-local-modification tasks before seed creation, but
	// doesn't perform any local modifications until all essential snaps are complete.
	firstLocalModApp := firstTaskAfterLocalModifications(c, appTS)
	c.Check(firstLocalModApp.WaitTasks(), testutil.Contains, lastEssentialSnapTask)

	// non-seed app performs its initial prerequisite check before seed creation,
	// but defers post-prerequisite work until all essential snaps are complete.
	firstPostPrereqsNonSeedApp := firstTaskAfterPrerequisites(c, nonSeedAppTS)
	c.Check(firstPostPrereqsNonSeedApp.WaitTasks(), testutil.Contains, lastEssentialSnapTask)

	// ensure that seed finalize task doesn't wait on non-seed snap
	nonSeedAppEndTask, err := nonSeedAppTS.Edge(snapstate.EndEdge)
	c.Assert(err, IsNil)
	c.Check(waitsOnTransitively(seedEnd, nonSeedAppEndTask), Equals, false)
}

func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshUndo(c *C) {
	_, restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{
		"kernel":         "kernel",
		"base":           "core18",
		"required-snaps": []any{"some-app"},
	}, []string{"kernel", "core18", "some-app"})
	defer restore()

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

	restartRequested := mockSeedRefreshRebootHandlers(s, c, fmt.Errorf("finalize seed refresh mock error"))

	s.installSeedRefreshSnaps(c,
		seedRefreshSnap{name: "kernel", snapID: "kernel-id", snapType: "kernel"},
		seedRefreshSnap{name: "core18", snapID: "core18-snap-id", snapType: "base"},
		seedRefreshSnap{name: "some-app", snapID: "some-app-id", snapType: "app", base: "core18"},
	)

	uts, chg := runSeedRefreshUpdate(c, s.state, s.user.ID, []snapstate.StoreUpdate{
		{InstanceName: "kernel"},
		{InstanceName: "core18"},
		{InstanceName: "some-app"},
	})
	taskSetsBySnap, seedTS := parseSeedRefreshTaskSets(uts)

	baseTS := mustTaskSetForSnap(c, taskSetsBySnap, "core18")
	kernelTS := mustTaskSetForSnap(c, taskSetsBySnap, "kernel")
	appTS := mustTaskSetForSnap(c, taskSetsBySnap, "some-app")

	c.Assert(seedTS, NotNil)
	c.Check(taskSetsShareLane(baseTS, kernelTS, appTS), Equals, true)
	c.Check(taskSetLanes(seedTS), testutil.DeepUnsortedMatches, taskSetLanes(baseTS))
	seedCreate, _, _ := splitSeedRefreshTasks(c, seedTS)

	// run the do path until create-recovery-system requests the seed-refresh reboot.
	s.settle(c)

	// simulate the reboot into the candidate recovery system and continue until
	// finalize-recovery-system fails, which should trigger undo for the seed cohort
	s.mockRestartAndSettle(c, chg)

	// let the undo path settle after the failure so task statuses reflect the
	// rollback state we want to verify
	s.mockRestartAndSettle(c, chg)

	assertLinkTaskStatus(c, baseTS, state.UndoneStatus)
	assertLinkTaskStatus(c, kernelTS, state.UndoneStatus)
	assertLinkTaskStatus(c, appTS, state.UndoneStatus)
	c.Check(seedCreate.Status(), Equals, state.UndoneStatus)

	// all snaps in the seed cohort are rolled back when finalize fails
	appLinkTask, err := appTS.Edge(snapstate.MaybeRebootEdge)
	c.Assert(err, IsNil)
	c.Check(appLinkTask.Status(), Equals, state.UndoneStatus)

	c.Check(chg.Status(), Equals, state.ErrorStatus)
	c.Check(chg.Err(), ErrorMatches, `(?s).*\(finalize seed refresh mock error\)`)
	c.Check(*restartRequested, DeepEquals, []restart.RestartType{restart.RestartSystem})
}

func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshWithSnapdAndNonSeedSnapRun(c *C) {
	_, restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{
		"kernel":         "kernel",
		"base":           "core18",
		"required-snaps": []any{"some-app"},
	}, []string{"snapd", "kernel", "core18", "some-app"})
	defer restore()

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

	restartRequested := mockSeedRefreshRebootHandlers(s, c, nil)

	s.installSeedRefreshSnaps(c,
		seedRefreshSnap{name: "snapd", snapID: "snapd-snap-id", snapType: "snapd"},
		seedRefreshSnap{name: "kernel", snapID: "kernel-id", snapType: "kernel"},
		seedRefreshSnap{name: "core18", snapID: "core18-snap-id", snapType: "base"},
		seedRefreshSnap{name: "some-app", snapID: "some-app-id", snapType: "app", base: "core18"},
		seedRefreshSnap{name: "some-other-snap", snapID: "some-other-snap-id", snapType: "app", base: "core18"},
	)

	uts, chg := runSeedRefreshUpdate(c, s.state, s.user.ID, []snapstate.StoreUpdate{
		{InstanceName: "snapd"},
		{InstanceName: "kernel"},
		{InstanceName: "core18"},
		{InstanceName: "some-app"},
		{InstanceName: "some-other-snap"},
	})

	taskSetsBySnap, seedTS := parseSeedRefreshTaskSets(uts)
	c.Assert(seedTS, NotNil)

	snapdTS := mustTaskSetForSnap(c, taskSetsBySnap, "snapd")
	baseTS := mustTaskSetForSnap(c, taskSetsBySnap, "core18")
	kernelTS := mustTaskSetForSnap(c, taskSetsBySnap, "kernel")
	appTS := mustTaskSetForSnap(c, taskSetsBySnap, "some-app")
	nonSeedAppTS := mustTaskSetForSnap(c, taskSetsBySnap, "some-other-snap")
	c.Check(taskSetsShareLane(snapdTS, baseTS, kernelTS, appTS), Equals, true)

	seedCreate, _, _ := splitSeedRefreshTasks(c, seedTS)

	snapdEnd, err := snapdTS.Edge(snapstate.EndEdge)
	c.Assert(err, IsNil)

	baseBegin, err := baseTS.Edge(snapstate.BeginEdge)
	c.Assert(err, IsNil)

	c.Check(baseBegin.WaitTasks(), testutil.Contains, snapdEnd)

	for _, ts := range []*state.TaskSet{snapdTS, baseTS, kernelTS, appTS} {
		lastBeforeLocal, err := ts.Edge(snapstate.LastBeforeLocalModificationsEdge)
		c.Assert(err, IsNil)
		c.Check(waitsOnTransitively(seedCreate, lastBeforeLocal), Equals, true)
	}

	nonSeedAppFirstPrereq, _ := findPrereqTasksForSnap(c, chg, "some-other-snap")
	nonSeedAppLastBefore, err := nonSeedAppTS.Edge(snapstate.LastBeforeLocalModificationsEdge)
	c.Assert(err, IsNil)
	c.Check(waitsOnTransitively(seedCreate, nonSeedAppFirstPrereq), Equals, true)
	c.Check(waitsOnTransitively(seedCreate, nonSeedAppLastBefore), Equals, false)
	for _, lane := range seedCreate.Lanes() {
		c.Check(nonSeedAppFirstPrereq.Lanes(), testutil.Contains, lane)
		c.Check(nonSeedAppLastBefore.Lanes(), Not(testutil.Contains), lane)
	}

	s.settle(c)
	s.mockRestartAndSettle(c, chg)

	c.Check(chg.Err(), IsNil)
	c.Check(chg.IsReady(), Equals, true)
	c.Check(*restartRequested, DeepEquals, []restart.RestartType{restart.RestartDaemon, restart.RestartSystem})
}

func (s *snapmgrTestSuite) testUpdateWithGoalSeedRefreshNonSeedSnapFailureDoesNotUndoSeed(c *C, failingTask string) {
	_, restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{
		"kernel":         "kernel",
		"base":           "core18",
		"required-snaps": []any{"some-app"},
	}, []string{"kernel", "core18", "some-app"})
	defer restore()

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

	restartRequested := mockSeedRefreshRebootHandlers(s, c, nil)

	s.installSeedRefreshSnaps(c,
		seedRefreshSnap{name: "kernel", snapID: "kernel-id", snapType: "kernel"},
		seedRefreshSnap{name: "core18", snapID: "core18-snap-id", snapType: "base"},
		seedRefreshSnap{name: "some-app", snapID: "some-app-id", snapType: "app", base: "core18"},
		seedRefreshSnap{name: "some-other-snap", snapID: "some-other-snap-id", snapType: "app", base: "core18"},
	)

	uts, chg := runSeedRefreshUpdate(c, s.state, s.user.ID, []snapstate.StoreUpdate{
		{InstanceName: "kernel"},
		{InstanceName: "core18"},
		{InstanceName: "some-app"},
		{InstanceName: "some-other-snap"},
	})
	taskSetsBySnap, seedTS := parseSeedRefreshTaskSets(uts)

	baseTS := mustTaskSetForSnap(c, taskSetsBySnap, "core18")
	kernelTS := mustTaskSetForSnap(c, taskSetsBySnap, "kernel")
	appTS := mustTaskSetForSnap(c, taskSetsBySnap, "some-app")
	nonSeedAppTS := mustTaskSetForSnap(c, taskSetsBySnap, "some-other-snap")

	c.Assert(seedTS, NotNil)
	c.Check(taskSetsShareLane(baseTS, kernelTS, appTS), Equals, true)
	c.Check(taskSetsShareLane(baseTS, nonSeedAppTS), Equals, false)
	c.Check(taskSetLanes(seedTS), testutil.DeepUnsortedMatches, taskSetLanes(baseTS))
	seedCreate, seedFinalize, _ := splitSeedRefreshTasks(c, seedTS)

	errInjected := 0
	s.fakeBackend.maybeInjectErr = func(op *fakeOp) error {
		if op.op == failingTask+":Doing" && op.name == "some-other-snap" {
			errInjected++
			return errors.New("non-seed snap mock error")
		}
		return nil
	}

	// run the do path until create-recovery-system requests the seed-refresh reboot.
	s.settle(c)

	// simulate the reboot into the candidate recovery system and continue until
	// the non-seed snap fails.
	s.mockRestartAndSettle(c, chg)
	assertTaskSetStatus(c, baseTS, state.DoneStatus)
	assertTaskSetStatus(c, kernelTS, state.DoneStatus)
	assertTaskSetStatus(c, appTS, state.DoneStatus)
	c.Check(seedCreate.Status(), Equals, state.DoneStatus)
	c.Check(seedFinalize.Status(), Equals, state.DoneStatus)
	assertTaskKindStatus(c, nonSeedAppTS, failingTask, state.ErrorStatus)

	c.Check(chg.Status(), Equals, state.ErrorStatus)
	c.Check(chg.Err(), ErrorMatches, `(?s).*\(non-seed snap mock error\)`)
	c.Check(*restartRequested, DeepEquals, []restart.RestartType{restart.RestartSystem})
	c.Check(errInjected, Equals, 1)
}

func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshNonSeedSnapFailureDoesNotUndoSeed(c *C) {
	s.testUpdateWithGoalSeedRefreshNonSeedSnapFailureDoesNotUndoSeed(c, "auto-connect")
}

func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshNonSeedSnapValidateFailureDoesNotUndoSeed(c *C) {
	s.testUpdateWithGoalSeedRefreshNonSeedSnapFailureDoesNotUndoSeed(c, "validate-snap")
}

func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshRemoveSystemFailureDoesNotUndoSeed(c *C) {
	_, restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{
		"kernel":         "kernel",
		"base":           "core18",
		"required-snaps": []any{"some-app"},
	}, nil)
	defer restore()

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

	oldSeedRefreshTasks := snapstate.SeedRefreshTasks
	snapstate.SeedRefreshTasks = func(st *state.State, _ snapstate.DeviceContext, candidates []snapstate.SeedRefreshCandidate, _ snapstate.SeedRefreshEvictionPolicy) (*snapstate.SeedRefreshTaskSet, map[string]bool, error) {
		added := make(map[string]bool, len(candidates))
		for _, candidate := range candidates {
			if candidate.InstanceName != "kernel" && candidate.InstanceName != "core18" && candidate.InstanceName != "some-app" {
				continue
			}
			added[candidate.InstanceName] = true
		}
		if len(added) == 0 {
			return nil, nil, nil
		}

		create := st.NewTask("create-recovery-system", "Create recovery system")
		restart.MarkTaskAsRestartBoundary(create, restart.RestartBoundaryDirectionDo)

		finalize := st.NewTask("finalize-recovery-system", "Finalize recovery system")
		finalize.WaitFor(create)

		remove1 := st.NewTask("remove-recovery-system", "Remove old recovery system 1")
		remove1.WaitFor(finalize)

		remove2 := st.NewTask("remove-recovery-system", "Remove old recovery system 2")
		remove2.WaitFor(finalize)

		return &snapstate.SeedRefreshTaskSet{
			Create:   create,
			Finalize: finalize,
			Remove:   []*state.Task{remove1, remove2},
		}, added, nil
	}
	defer func() {
		snapstate.SeedRefreshTasks = oldSeedRefreshTasks
	}()

	restartRequested := mockSeedRefreshRebootHandlers(s, c, nil)
	var mu sync.Mutex
	var removeCalls []string
	s.o.TaskRunner().AddHandler("remove-recovery-system", func(t *state.Task, _ *tomb.Tomb) error {
		mu.Lock()
		defer mu.Unlock()
		removeCalls = append(removeCalls, t.Summary())
		if len(removeCalls) == 1 {
			return fmt.Errorf("remove seed mock error")
		}
		return nil
	}, nil)

	s.installSeedRefreshSnaps(c,
		seedRefreshSnap{name: "kernel", snapID: "kernel-id", snapType: "kernel"},
		seedRefreshSnap{name: "core18", snapID: "core18-snap-id", snapType: "base"},
		seedRefreshSnap{name: "some-app", snapID: "some-app-id", snapType: "app", base: "core18"},
	)

	uts, chg := runSeedRefreshUpdate(c, s.state, s.user.ID, []snapstate.StoreUpdate{
		{InstanceName: "kernel"},
		{InstanceName: "core18"},
		{InstanceName: "some-app"},
	})
	taskSetsBySnap, seedTS := parseSeedRefreshTaskSets(uts)

	baseTS := mustTaskSetForSnap(c, taskSetsBySnap, "core18")
	kernelTS := mustTaskSetForSnap(c, taskSetsBySnap, "kernel")
	appTS := mustTaskSetForSnap(c, taskSetsBySnap, "some-app")

	c.Assert(seedTS, NotNil)
	c.Check(taskSetsShareLane(baseTS, kernelTS, appTS), Equals, true)

	seedCreate, seedFinalize, seedRemovals := splitSeedRefreshTasks(c, seedTS)
	c.Assert(seedRemovals, HasLen, 2)
	c.Check(seedRemovals[0].WaitTasks(), DeepEquals, []*state.Task{seedFinalize})
	c.Check(seedRemovals[1].WaitTasks(), DeepEquals, []*state.Task{seedFinalize})

	// seed removals exist in their own lanes. that means we won't attempt to
	// roll anything back if these tasks fail, and a failed cleanup will not
	// abort sibling cleanup tasks.
	seedLanes := taskSetLanes(baseTS)
	removalLanes := make(map[int]bool, len(seedRemovals))
	for _, remove := range seedRemovals {
		c.Assert(remove.Lanes(), HasLen, 1)
		lane := remove.Lanes()[0]
		removalLanes[lane] = true
		c.Check(seedLanes, Not(testutil.Contains), lane)
	}
	c.Check(removalLanes, HasLen, 2)

	// run the do path until create-recovery-system requests the seed-refresh reboot.
	s.settle(c)

	// simulate the reboot into the candidate recovery system and continue until
	// pruning fails after the seed cohort has completed.
	s.mockRestartAndSettle(c, chg)

	assertTaskSetStatus(c, baseTS, state.DoneStatus)
	assertTaskSetStatus(c, kernelTS, state.DoneStatus)
	assertTaskSetStatus(c, appTS, state.DoneStatus)
	c.Check(seedCreate.Status(), Equals, state.DoneStatus)
	c.Check(seedFinalize.Status(), Equals, state.DoneStatus)

	failed := 0
	done := 0
	for _, remove := range seedRemovals {
		switch remove.Status() {
		case state.ErrorStatus:
			failed++
		case state.DoneStatus:
			done++
		default:
			c.Fatalf("unexpected removal task status: %v", remove.Status())
		}
	}
	c.Check(failed, Equals, 1)
	c.Check(done, Equals, 1)

	c.Check(removeCalls, testutil.DeepUnsortedMatches, []string{"Remove old recovery system 1", "Remove old recovery system 2"})

	c.Check(chg.Status(), Equals, state.ErrorStatus)
	c.Check(chg.Err(), ErrorMatches, `(?s).*\(remove seed mock error\)`)
	c.Check(*restartRequested, DeepEquals, []restart.RestartType{restart.RestartSystem})
}

func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshKernelPostRebootFailureUndoesSeed(c *C) {
	_, restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{
		"kernel":         "kernel",
		"base":           "core18",
		"required-snaps": []any{"some-app"},
	}, []string{"kernel", "core18", "some-app"})
	defer restore()

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

	restartRequested := mockSeedRefreshRebootHandlers(s, c, nil)

	s.installSeedRefreshSnaps(c,
		seedRefreshSnap{name: "kernel", snapID: "kernel-id", snapType: "kernel"},
		seedRefreshSnap{name: "core18", snapID: "core18-snap-id", snapType: "base"},
		seedRefreshSnap{name: "some-app", snapID: "some-app-id", snapType: "app", base: "core18"},
	)

	uts, chg := runSeedRefreshUpdate(c, s.state, s.user.ID, []snapstate.StoreUpdate{
		{InstanceName: "kernel"},
		{InstanceName: "core18"},
		{InstanceName: "some-app"},
	})
	taskSetsBySnap, seedTS := parseSeedRefreshTaskSets(uts)

	baseTS := mustTaskSetForSnap(c, taskSetsBySnap, "core18")
	kernelTS := mustTaskSetForSnap(c, taskSetsBySnap, "kernel")
	appTS := mustTaskSetForSnap(c, taskSetsBySnap, "some-app")

	c.Assert(seedTS, NotNil)
	c.Check(taskSetsShareLane(baseTS, kernelTS, appTS), Equals, true)
	c.Check(taskSetLanes(seedTS), testutil.DeepUnsortedMatches, taskSetLanes(baseTS))
	seedCreate, seedFinalize, _ := splitSeedRefreshTasks(c, seedTS)

	errInjected := 0
	s.fakeBackend.maybeInjectErr = func(op *fakeOp) error {
		if op.op == "auto-connect:Doing" && op.name == "kernel" {
			errInjected++
			return fmt.Errorf("auto-connect-kernel mock error")
		}
		return nil
	}

	// run the do path until create-recovery-system requests the seed-refresh reboot.
	s.settle(c)

	// simulate the reboot into the candidate recovery system and continue until
	// the kernel post-reboot chain fails, which should trigger undo for the seed cohort
	s.mockRestartAndSettle(c, chg)

	// let the undo path settle after the failure so task statuses reflect the
	// rollback state we want to verify
	s.mockRestartAndSettle(c, chg)

	assertLinkTaskStatus(c, baseTS, state.UndoneStatus)
	assertLinkTaskStatus(c, kernelTS, state.UndoneStatus)
	c.Check(seedCreate.Status(), Equals, state.UndoneStatus)
	c.Check(seedFinalize.Status(), Equals, state.HoldStatus)

	// non-essential snaps don't continue after the essential seed cohort rolls back
	appLinkTask, err := appTS.Edge(snapstate.MaybeRebootEdge)
	c.Assert(err, IsNil)
	c.Check(appLinkTask.Status(), Equals, state.HoldStatus)

	c.Check(chg.Status(), Equals, state.ErrorStatus)
	c.Check(chg.Err(), ErrorMatches, `(?s).*\(auto-connect-kernel mock error\)`)
	c.Check(*restartRequested, DeepEquals, []restart.RestartType{restart.RestartSystem})
	c.Check(errInjected, Equals, 1)
}

func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshNoEssentials(c *C) {
	_, restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{
		"kernel":         "kernel",
		"base":           "core18",
		"required-snaps": []any{"some-app"},
	}, []string{"some-app"})
	defer restore()

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

	s.installSeedRefreshSnaps(c,
		seedRefreshSnap{name: "core18", snapID: "core18-snap-id", snapType: "base"},
		seedRefreshSnap{name: "some-app", snapID: "some-app-id", snapType: "app", base: "core18"},
	)

	uts, _ := runSeedRefreshUpdate(c, s.state, s.user.ID, []snapstate.StoreUpdate{{InstanceName: "some-app"}})
	taskSetsBySnap, seedTS := parseSeedRefreshTaskSets(uts)

	appTS := mustTaskSetForSnap(c, taskSetsBySnap, "some-app")

	c.Assert(seedTS, NotNil)
	c.Check(taskSetLanes(seedTS), testutil.DeepUnsortedMatches, taskSetLanes(appTS))

	seedCreate, seedEnd, _ := splitSeedRefreshTasks(c, seedTS)

	appLastBefore, err := appTS.Edge(snapstate.LastBeforeLocalModificationsEdge)
	c.Assert(err, IsNil)
	c.Check(seedCreate.WaitTasks(), testutil.Contains, appLastBefore)
	appEndTask, err := appTS.Edge(snapstate.EndEdge)
	c.Assert(err, IsNil)
	c.Check(waitsOnTransitively(seedEnd, appEndTask), Equals, true)
}

func setupSeedRefreshRevertSnapOfType(c *C, st *state.State, spec seedRefreshSnap) (*snap.SideInfo, *snap.SideInfo) {
	oldSideInfo := &snap.SideInfo{RealName: spec.name, SnapID: spec.snapID, Revision: snap.R(5)}
	newSideInfo := &snap.SideInfo{RealName: spec.name, SnapID: spec.snapID, Revision: snap.R(7)}

	var oldSnapYaml strings.Builder
	fmt.Fprintf(&oldSnapYaml, "name: %s\nversion: 1.0\n", spec.name)
	if spec.snapType != "" {
		fmt.Fprintf(&oldSnapYaml, "type: %s\n", spec.snapType)
	}
	if spec.base != "" {
		fmt.Fprintf(&oldSnapYaml, "base: %s\n", spec.base)
	}
	snaptest.MockSnap(c, oldSnapYaml.String(), oldSideInfo)

	var newSnapYaml strings.Builder
	fmt.Fprintf(&newSnapYaml, "name: %s\nversion: 2.0\n", spec.name)
	if spec.snapType != "" {
		fmt.Fprintf(&newSnapYaml, "type: %s\n", spec.snapType)
	}
	if spec.base != "" {
		fmt.Fprintf(&newSnapYaml, "base: %s\n", spec.base)
	}
	snaptest.MockSnap(c, newSnapYaml.String(), newSideInfo)

	snapstate.Set(st, spec.name, &snapstate.SnapState{
		Active:          true,
		Sequence:        snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{oldSideInfo, newSideInfo}),
		Current:         newSideInfo.Revision,
		TrackingChannel: "latest/stable",
		SnapType:        spec.snapType,
		Base:            spec.base,
	})

	return oldSideInfo, newSideInfo
}

func makeSeedRefreshLocalSnapPath(c *C, spec seedRefreshSnap, rev snap.Revision) (*snap.SideInfo, string) {
	sideInfo := &snap.SideInfo{RealName: spec.name, SnapID: spec.snapID, Revision: rev}

	var snapYaml strings.Builder
	fmt.Fprintf(&snapYaml, "name: %s\nversion: %s\nepoch: 1*\n", spec.name, rev)
	if spec.snapType != "" {
		fmt.Fprintf(&snapYaml, "type: %s\n", spec.snapType)
	}
	if spec.base != "" {
		fmt.Fprintf(&snapYaml, "base: %s\n", spec.base)
	}

	path, _ := snaptest.MakeTestSnapInfoWithFiles(c, snapYaml.String(), nil, sideInfo)
	return sideInfo, path
}

func (s *snapmgrTestSuite) testRevertSeedRefreshRunThrough(c *C, spec seedRefreshSnap, model map[string]any) {
	observed, restore := s.setupSeedRefreshUpdateTest(c, false, true, model, []string{spec.name})
	defer restore()

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

	restartRequested := mockSeedRefreshRebootHandlers(s, c, nil)

	if spec.snapType == "gadget" {
		s.o.TaskRunner().AddHandler("update-gadget-cmdline", func(t *state.Task, _ *tomb.Tomb) error {
			st := t.State()
			st.Lock()
			defer st.Unlock()
			t.SetStatus(state.DoneStatus)
			return nil
		}, nil)
	}

	oldSideInfo, _ := setupSeedRefreshRevertSnapOfType(c, s.state, spec)

	ts, err := snapstate.RevertToRevision(s.state, spec.name, oldSideInfo.Revision, snapstate.Flags{}, "")
	c.Assert(err, IsNil)

	chg := s.state.NewChange("revert", "revert a snap backwards")
	chg.AddAll(ts)
	c.Assert(chg.CheckTaskDependencies(), IsNil)

	setupTask, err := ts.Edge(snapstate.SnapSetupEdge)
	c.Assert(err, IsNil)
	snapsup, err := snapstate.TaskSnapSetup(setupTask)
	c.Assert(err, IsNil)
	c.Check(snapsup.Revert, Equals, true)
	c.Check(snapsup.Revision(), Equals, oldSideInfo.Revision)
	c.Assert(observed.initial, HasLen, 1)
	c.Check(observed.initial[0], testutil.DeepUnsortedMatches, []snapstate.SeedRefreshCandidate{{
		InstanceName:     spec.name,
		SnapSetupTaskIDs: []string{setupTask.ID()},
	}})
	c.Check(observed.evictions, DeepEquals, []snapstate.SeedRefreshEvictionPolicy{{
		SeedsToRetain: 1,
		ReplaceLatest: true,
	}})

	seedCreate, seedFinalize, _ := splitSeedRefreshTasks(c, ts)
	lastBeforeLocal, err := ts.Edge(snapstate.LastBeforeLocalModificationsEdge)
	c.Assert(err, IsNil)
	c.Check(waitsOnTransitively(seedCreate, lastBeforeLocal), Equals, true)

	endTask, err := ts.Edge(snapstate.EndEdge)
	c.Assert(err, IsNil)
	c.Check(waitsOnTransitively(seedFinalize, endTask), Equals, true)

	linkTask, err := ts.Edge(snapstate.MaybeRebootEdge)
	c.Assert(err, IsNil)
	c.Check(hasDoRestartBoundary(seedCreate), Equals, true)
	c.Check(hasDoRestartBoundary(linkTask), Equals, false)

	s.settle(c)

	c.Check(restart.Pending(s.state), Equals, restart.RestartSystem)

	s.mockRestartAndSettle(c, chg)

	c.Assert(chg.Err(), IsNil)
	c.Assert(chg.IsReady(), Equals, true)
	expectedRestarts := []restart.RestartType{restart.RestartSystem}
	if spec.snapType == "snapd" {
		expectedRestarts = []restart.RestartType{restart.RestartDaemon, restart.RestartSystem}
	}
	c.Check(*restartRequested, DeepEquals, expectedRestarts)

	var snapst snapstate.SnapState
	c.Assert(snapstate.Get(s.state, spec.name, &snapst), IsNil)
	c.Check(snapst.Current, Equals, oldSideInfo.Revision)
}

func (s *snapmgrTestSuite) TestRevertSeedRefreshSnapdRunThrough(c *C) {
	s.testRevertSeedRefreshRunThrough(c, seedRefreshSnap{
		name:     "snapd",
		snapID:   "snapd-snap-id",
		snapType: "snapd",
	}, map[string]any{
		"kernel": "kernel",
		"base":   "core18",
	})
}

func (s *snapmgrTestSuite) TestRevertSeedRefreshBaseRunThrough(c *C) {
	s.testRevertSeedRefreshRunThrough(c, seedRefreshSnap{
		name:     "core18",
		snapID:   "core18-snap-id",
		snapType: "base",
	}, map[string]any{
		"kernel": "kernel",
		"base":   "core18",
	})
}

func (s *snapmgrTestSuite) TestRevertSeedRefreshGadgetRunThrough(c *C) {
	s.testRevertSeedRefreshRunThrough(c, seedRefreshSnap{
		name:     "brand-gadget",
		snapID:   "brand-gadget-id",
		snapType: "gadget",
	}, map[string]any{
		"gadget": "brand-gadget",
		"kernel": "kernel",
		"base":   "core18",
	})
}

func (s *snapmgrTestSuite) TestRevertSeedRefreshKernelRunThrough(c *C) {
	s.testRevertSeedRefreshRunThrough(c, seedRefreshSnap{
		name:     "kernel",
		snapID:   "kernel-id",
		snapType: "kernel",
	}, map[string]any{
		"kernel": "kernel",
		"base":   "core18",
	})
}

func (s *snapmgrTestSuite) TestRevertSeedRefreshAppRunThrough(c *C) {
	s.testRevertSeedRefreshRunThrough(c, seedRefreshSnap{
		name:     "some-app",
		snapID:   "some-app-id",
		snapType: "app",
		base:     "core18",
	}, map[string]any{
		"kernel":         "kernel",
		"base":           "core18",
		"required-snaps": []any{"some-app"},
	})
}

func (s *snapmgrTestSuite) TestRevertSeedRefreshBlockedByOtherChanges(c *C) {
	_, restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{
		"kernel":         "kernel",
		"base":           "core18",
		"required-snaps": []any{"some-app"},
	}, []string{"some-app"})
	defer restore()

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

	s.installSeedRefreshSnaps(c,
		seedRefreshSnap{name: "core18", snapID: "core18-snap-id", snapType: "base"},
	)
	oldSideInfo, _ := setupSeedRefreshRevertSnapOfType(c, s.state, seedRefreshSnap{
		name:     "some-app",
		snapID:   "some-app-id",
		snapType: "app",
		base:     "core18",
	})

	chg := s.state.NewChange("unrelated", "...")
	chg.AddTask(s.state.NewTask("task", "..."))

	_, err := snapstate.RevertToRevision(s.state, "some-app", oldSideInfo.Revision, snapstate.Flags{}, "")
	c.Assert(err, FitsTypeOf, &snapstate.ChangeConflictError{})
	c.Check(err, ErrorMatches, `other changes in progress \(conflicting change "unrelated"\), change "seed refresh" not allowed until they are done`)
}

func (s *snapmgrTestSuite) TestRevertSeedRefreshNoopWhenSnapNotSelected(c *C) {
	observed, restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{
		"kernel":         "kernel",
		"base":           "core18",
		"required-snaps": []any{"some-app"},
	}, nil)
	defer restore()

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

	s.installSeedRefreshSnaps(c,
		seedRefreshSnap{name: "core18", snapID: "core18-snap-id", snapType: "base"},
	)
	oldSideInfo, _ := setupSeedRefreshRevertSnapOfType(c, s.state, seedRefreshSnap{
		name:     "some-app",
		snapID:   "some-app-id",
		snapType: "app",
		base:     "core18",
	})

	ts, err := snapstate.RevertToRevision(s.state, "some-app", oldSideInfo.Revision, snapstate.Flags{}, "")
	c.Assert(err, IsNil)

	setupTask, err := ts.Edge(snapstate.SnapSetupEdge)
	c.Assert(err, IsNil)
	snapsup, err := snapstate.TaskSnapSetup(setupTask)
	c.Assert(err, IsNil)
	c.Check(snapsup.Revert, Equals, true)
	c.Check(snapsup.Revision(), Equals, oldSideInfo.Revision)

	c.Assert(observed.initial, HasLen, 1)
	c.Check(observed.initial[0], testutil.DeepUnsortedMatches, []snapstate.SeedRefreshCandidate{{
		InstanceName:     "some-app",
		SnapSetupTaskIDs: []string{setupTask.ID()},
	}})
	c.Check(observed.evictions, DeepEquals, []snapstate.SeedRefreshEvictionPolicy{{SeedsToRetain: 1, ReplaceLatest: true}})
	c.Check(countTasksOfKind(ts.Tasks(), "create-recovery-system"), Equals, 0)
	c.Check(countTasksOfKind(ts.Tasks(), "finalize-recovery-system"), Equals, 0)
}

func (s *snapmgrTestSuite) TestInstallPathSeedRefreshRunThrough(c *C) {
	observed, restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{
		"kernel": "kernel",
		"base":   "core18",
	}, []string{"kernel"})
	defer restore()

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

	s.installSeedRefreshSnaps(c,
		seedRefreshSnap{name: "core18", snapID: "core18-snap-id", snapType: "base"},
		seedRefreshSnap{name: "kernel", snapID: "kernel-id", snapType: "kernel"},
	)

	newSideInfo, path := makeSeedRefreshLocalSnapPath(c, seedRefreshSnap{
		name:     "kernel",
		snapID:   "kernel-id",
		snapType: "kernel",
	}, snap.R(11))

	ts, err := snapstate.InstallPath(s.state, newSideInfo, path, "kernel", "", snapstate.Flags{}, nil)
	c.Assert(err, IsNil)
	c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks()))

	chg := s.state.NewChange("install", "install snap from path")
	chg.AddAll(ts)
	c.Assert(chg.CheckTaskDependencies(), IsNil)

	setupTask, err := ts.Edge(snapstate.SnapSetupEdge)
	c.Assert(err, IsNil)
	snapsup, err := snapstate.TaskSnapSetup(setupTask)
	c.Assert(err, IsNil)
	c.Check(snapsup.InstanceName(), Equals, "kernel")
	c.Check(snapsup.Revision(), Equals, newSideInfo.Revision)
	c.Check(snapsup.SnapPath, Equals, path)

	c.Assert(observed.initial, HasLen, 1)
	c.Check(observed.initial[0], testutil.DeepUnsortedMatches, []snapstate.SeedRefreshCandidate{{
		InstanceName:     "kernel",
		SnapSetupTaskIDs: []string{setupTask.ID()},
	}})
	c.Check(observed.evictions, DeepEquals, []snapstate.SeedRefreshEvictionPolicy{{SeedsToRetain: 1}})

	seedCreate, seedFinalize, _ := splitSeedRefreshTasks(c, ts)
	lastBeforeLocal, err := ts.Edge(snapstate.LastBeforeLocalModificationsEdge)
	c.Assert(err, IsNil)
	c.Check(waitsOnTransitively(seedCreate, lastBeforeLocal), Equals, true)

	endTask, err := ts.Edge(snapstate.EndEdge)
	c.Assert(err, IsNil)
	c.Check(waitsOnTransitively(seedFinalize, endTask), Equals, true)

	linkTask, err := ts.Edge(snapstate.MaybeRebootEdge)
	c.Assert(err, IsNil)
	c.Check(hasDoRestartBoundary(seedCreate), Equals, true)
	c.Check(hasDoRestartBoundary(linkTask), Equals, false)
}

func (s *snapmgrTestSuite) TestInstallPathSeedRefreshNoopWhenSnapNotSelected(c *C) {
	observed, restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{
		"kernel": "kernel",
		"base":   "core18",
	}, nil)
	defer restore()

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

	s.installSeedRefreshSnaps(c,
		seedRefreshSnap{name: "core18", snapID: "core18-snap-id", snapType: "base"},
		seedRefreshSnap{name: "kernel", snapID: "kernel-id", snapType: "kernel"},
	)

	newSideInfo, path := makeSeedRefreshLocalSnapPath(c, seedRefreshSnap{
		name:     "kernel",
		snapID:   "kernel-id",
		snapType: "kernel",
	}, snap.R(11))

	ts, err := snapstate.InstallPath(s.state, newSideInfo, path, "kernel", "", snapstate.Flags{}, nil)
	c.Assert(err, IsNil)

	setupTask, err := ts.Edge(snapstate.SnapSetupEdge)
	c.Assert(err, IsNil)
	c.Assert(observed.initial, HasLen, 1)
	c.Check(observed.initial[0], testutil.DeepUnsortedMatches, []snapstate.SeedRefreshCandidate{{
		InstanceName:     "kernel",
		SnapSetupTaskIDs: []string{setupTask.ID()},
	}})
	c.Check(observed.evictions, DeepEquals, []snapstate.SeedRefreshEvictionPolicy{{SeedsToRetain: 1}})
	c.Check(countTasksOfKind(ts.Tasks(), "create-recovery-system"), Equals, 0)
	c.Check(countTasksOfKind(ts.Tasks(), "finalize-recovery-system"), Equals, 0)
}

func (s *snapmgrTestSuite) TestInstallPathSeedRefreshBlockedByOtherChanges(c *C) {
	_, restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{
		"kernel": "kernel",
		"base":   "core18",
	}, []string{"kernel"})
	defer restore()

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

	s.installSeedRefreshSnaps(c,
		seedRefreshSnap{name: "core18", snapID: "core18-snap-id", snapType: "base"},
		seedRefreshSnap{name: "kernel", snapID: "kernel-id", snapType: "kernel"},
	)

	newSideInfo, path := makeSeedRefreshLocalSnapPath(c, seedRefreshSnap{
		name:     "kernel",
		snapID:   "kernel-id",
		snapType: "kernel",
	}, snap.R(11))

	chg := s.state.NewChange("unrelated", "...")
	chg.AddTask(s.state.NewTask("task", "..."))

	_, err := snapstate.InstallPath(s.state, newSideInfo, path, "kernel", "", snapstate.Flags{}, nil)
	c.Assert(err, FitsTypeOf, &snapstate.ChangeConflictError{})
	c.Check(err, ErrorMatches, `other changes in progress \(conflicting change "unrelated"\), change "seed refresh" not allowed until they are done`)
}

func (s *snapmgrTestSuite) TestTryPathSkipsSeedRefresh(c *C) {
	observed, restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{
		"kernel":         "kernel",
		"base":           "core18",
		"required-snaps": []any{"some-app"},
	}, []string{"some-app"})
	defer restore()

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

	s.installSeedRefreshSnaps(c,
		seedRefreshSnap{name: "core18", snapID: "core18-snap-id", snapType: "base"},
	)

	tryDir := c.MkDir()
	c.Assert(os.Chmod(tryDir, 0755), IsNil)
	snapYaml := filepath.Join(tryDir, "meta", "snap.yaml")
	c.Assert(os.MkdirAll(filepath.Dir(snapYaml), 0755), IsNil)
	c.Assert(os.WriteFile(snapYaml, []byte("name: some-app\nversion: 1.0\nbase: core18\n"), 0644), IsNil)

	ts, err := snapstate.TryPath(s.state, "some-app", tryDir, snapstate.Flags{})
	c.Assert(err, IsNil)
	c.Check(countTasksOfKind(ts.Tasks(), "create-recovery-system"), Equals, 0)
	c.Check(countTasksOfKind(ts.Tasks(), "finalize-recovery-system"), Equals, 0)
	c.Check(observed.initial, HasLen, 0)
	c.Check(observed.evictions, HasLen, 0)
}

func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshNoEssentialsWithAdditionalComponents(c *C) {
	observed, restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{
		"kernel":         "kernel",
		"base":           "core18",
		"required-snaps": []any{"some-app"},
	}, []string{"some-app"})
	defer restore()

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

	s.installSeedRefreshSnaps(c,
		seedRefreshSnap{name: "core18", snapID: "core18-snap-id", snapType: "base"},
		seedRefreshSnap{name: "some-app", snapID: "some-app-id", snapType: "app", base: "core18", channel: "channel-for-components"},
	)

	s.fakeStore.snapResourcesFn = func(info *snap.Info) []store.SnapResourceResult {
		if info.InstanceName() != "some-app" {
			return nil
		}

		return []store.SnapResourceResult{{
			DownloadInfo: snap.DownloadInfo{
				DownloadURL: "http://example.com/standard-component",
			},
			Name:      "standard-component",
			Revision:  5,
			Type:      "component/standard",
			Version:   "1.0",
			CreatedAt: "2024-01-01T00:00:00Z",
		}}
	}

	uts, _ := runSeedRefreshUpdate(c, s.state, s.user.ID, []snapstate.StoreUpdate{{
		InstanceName:         "some-app",
		AdditionalComponents: []string{"standard-component"},
	}})
	taskSetsBySnap, seedTS := parseSeedRefreshTaskSets(uts)

	appTS := mustTaskSetForSnap(c, taskSetsBySnap, "some-app")
	appSnapSetupTask, err := appTS.Edge(snapstate.SnapSetupEdge)
	c.Assert(err, IsNil)

	var appCompSetupTaskIDs []string
	c.Assert(appSnapSetupTask.Get("component-setup-tasks", &appCompSetupTaskIDs), IsNil)
	c.Assert(appCompSetupTaskIDs, HasLen, 1)
	c.Assert(observed.initial, HasLen, 1)
	c.Check(observed.initial[0], testutil.DeepUnsortedMatches, []snapstate.SeedRefreshCandidate{{
		InstanceName:          "some-app",
		SnapSetupTaskIDs:      []string{appSnapSetupTask.ID()},
		ComponentSetupTaskIDs: appCompSetupTaskIDs,
	}})
	c.Check(observed.evictions, DeepEquals, []snapstate.SeedRefreshEvictionPolicy{{SeedsToRetain: 1}})

	c.Assert(seedTS, NotNil)
	c.Check(taskSetLanes(seedTS), testutil.DeepUnsortedMatches, taskSetLanes(appTS))

	seedCreate, seedEnd, _ := splitSeedRefreshTasks(c, seedTS)

	appLastBefore, err := appTS.Edge(snapstate.LastBeforeLocalModificationsEdge)
	c.Assert(err, IsNil)
	c.Check(seedCreate.WaitTasks(), testutil.Contains, appLastBefore)

	appEndTask, err := appTS.Edge(snapstate.EndEdge)
	c.Assert(err, IsNil)
	c.Check(waitsOnTransitively(seedEnd, appEndTask), Equals, true)
}

func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshSkippedWhileParentSeedRefreshNotReady(c *C) {
	_, restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{
		"kernel":         "kernel",
		"base":           "core18",
		"required-snaps": []any{"some-app", "content-provider"},
	}, []string{"some-app"})
	defer restore()

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

	s.installSeedRefreshSnaps(c,
		seedRefreshSnap{name: "kernel", snapID: "kernel-id", snapType: "kernel"},
		seedRefreshSnap{name: "core18", snapID: "core18-snap-id", snapType: "base"},
		seedRefreshSnap{name: "some-app", snapID: "some-app-id", snapType: "app", base: "core18"},
		seedRefreshSnap{name: "content-provider", snapID: "content-provider-id", snapType: "app", base: "core18"},
	)

	uts, chg := runSeedRefreshUpdate(c, s.state, s.user.ID, []snapstate.StoreUpdate{{InstanceName: "some-app"}})
	c.Assert(findSeedRefreshTaskSet(uts.Refresh), NotNil)
	c.Assert(countTasksOfKind(chg.Tasks(), "create-recovery-system"), Equals, 1)
	c.Assert(countTasksOfKind(chg.Tasks(), "finalize-recovery-system"), Equals, 1)

	goal := snapstate.StoreUpdateGoal(snapstate.StoreUpdate{InstanceName: "content-provider"})
	ts, err := snapstate.UpdateOne(context.Background(), s.state, goal, nil, snapstate.Options{
		UserID:          s.user.ID,
		ConflictOptions: snapstate.ConflictOptions{FromChange: chg.ID()},
	})
	c.Assert(err, IsNil)

	c.Check(countTasksOfKind(ts.Tasks(), "create-recovery-system"), Equals, 0)
	c.Check(countTasksOfKind(ts.Tasks(), "finalize-recovery-system"), Equals, 0)
}

func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshAllowedOnceParentSeedRefreshReady(c *C) {
	_, restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{
		"kernel": "kernel",
		"base":   "core18",
	}, []string{"core18"})
	defer restore()

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

	s.installSeedRefreshSnaps(c,
		seedRefreshSnap{name: "kernel", snapID: "kernel-id", snapType: "kernel"},
		seedRefreshSnap{name: "core18", snapID: "core18-snap-id", snapType: "base"},
	)

	uts, chg := runSeedRefreshUpdate(c, s.state, s.user.ID, []snapstate.StoreUpdate{{InstanceName: "core18"}})
	seedTS := findSeedRefreshTaskSet(uts.Refresh)
	c.Assert(seedTS, NotNil)

	seedCreate, seedFinalize, _ := splitSeedRefreshTasks(c, seedTS)
	seedCreate.SetStatus(state.DoneStatus)
	seedFinalize.SetStatus(state.DoneStatus)

	goal := snapstate.StoreUpdateGoal(snapstate.StoreUpdate{InstanceName: "core18"})
	ts, err := snapstate.UpdateOne(context.Background(), s.state, goal, nil, snapstate.Options{
		UserID:          s.user.ID,
		ConflictOptions: snapstate.ConflictOptions{FromChange: chg.ID()},
	})
	c.Assert(err, IsNil)

	c.Check(countTasksOfKind(ts.Tasks(), "create-recovery-system"), Equals, 1)
	c.Check(countTasksOfKind(ts.Tasks(), "finalize-recovery-system"), Equals, 1)
}

func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshReRefreshCreatesSecondSeed(c *C) {
	observed, restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{
		"kernel":         "kernel",
		"base":           "core18",
		"required-snaps": []any{"some-app"},
	}, []string{"some-app"})
	defer restore()

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

	restartRequested := mockSeedRefreshRebootHandlers(s, c, nil)

	s.installSeedRefreshSnaps(c,
		seedRefreshSnap{name: "kernel", snapID: "kernel-id", snapType: "kernel"},
		seedRefreshSnap{name: "core18", snapID: "core18-snap-id", snapType: "base"},
		seedRefreshSnap{name: "some-app", snapID: "some-app-id", snapType: "app", base: "core18"},
	)

	s.fakeStore.refreshRevnos = map[string]snap.Revision{"some-app-id": snap.R(11)}
	uts, chg := runSeedRefreshUpdate(c, s.state, s.user.ID, []snapstate.StoreUpdate{{InstanceName: "some-app"}})
	c.Assert(findSeedRefreshTaskSet(uts.Refresh), NotNil)

	var rerefreshCalls int
	defer snapstate.MockReRefreshUpdateMany(func(ctx context.Context, st *state.State, snaps []string, _ []*snapstate.RevisionOptions, userID int, _ snapstate.UpdateFilter, flags *snapstate.Flags, fromChange string) ([]string, *snapstate.UpdateTaskSets, error) {
		rerefreshCalls++
		c.Check(snaps, DeepEquals, []string{"some-app"})
		c.Check(fromChange, Equals, chg.ID())

		s.fakeStore.refreshRevnos["some-app-id"] = snap.R(12)
		goal := snapstate.StoreUpdateGoal(snapstate.StoreUpdate{InstanceName: "some-app"})
		return snapstate.UpdateWithGoal(ctx, st, goal, nil, snapstate.Options{
			UserID:          userID,
			Flags:           *flags,
			ConflictOptions: snapstate.ConflictOptions{FromChange: fromChange},
		})
	})()

	// initial refresh reaches the first seed reboot boundary.
	s.settle(c)
	c.Check(restart.Pending(s.state), Equals, restart.RestartSystem)
	c.Check(countTasksOfKind(chg.Tasks(), "create-recovery-system"), Equals, 1)
	c.Check(countTasksOfKind(chg.Tasks(), "finalize-recovery-system"), Equals, 1)

	// after the first reboot, check-rerefresh adds the second refresh and reaches
	// the next seed reboot boundary.
	s.mockRestartAndSettle(c, chg)
	c.Check(restart.Pending(s.state), Equals, restart.RestartSystem)
	c.Check(countTasksOfKind(chg.Tasks(), "create-recovery-system"), Equals, 2)
	c.Check(countTasksOfKind(chg.Tasks(), "finalize-recovery-system"), Equals, 2)
	c.Assert(observed.initial, HasLen, 2)
	c.Check(observed.evictions, DeepEquals, []snapstate.SeedRefreshEvictionPolicy{
		{SeedsToRetain: 1},
		{SeedsToRetain: 1},
	})
	c.Assert(observed.initial, HasLen, 2)
	c.Assert(observed.initial[0], HasLen, 1)
	c.Check(observed.initial[0][0].InstanceName, Equals, "some-app")
	c.Assert(observed.initial[1], HasLen, 1)
	c.Check(observed.initial[1][0].InstanceName, Equals, "some-app")

	// the second reboot completes the re-refresh seed update.
	s.mockRestartAndSettle(c, chg)

	c.Assert(chg.Err(), IsNil, Commentf("change tasks:\n%s", printTasks(chg.Tasks())))
	c.Assert(chg.IsReady(), Equals, true, Commentf("change tasks:\n%s", printTasks(chg.Tasks())))
	c.Check(*restartRequested, DeepEquals, []restart.RestartType{restart.RestartSystem, restart.RestartSystem})

	var someAppState snapstate.SnapState
	err := snapstate.Get(s.state, "some-app", &someAppState)
	c.Assert(err, IsNil)
	c.Check(someAppState.Current, Equals, snap.R(12))
}

func (s *snapmgrTestSuite) TestUpdateOneIncludeFromChangeInTaskConflictCheckIgnoresSameChangeSeedRefreshExclusivity(c *C) {
	_, restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{
		"kernel":         "kernel",
		"base":           "core18",
		"required-snaps": []any{"content-provider"},
	}, []string{"kernel", "core18", "content-provider"})
	defer restore()

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

	s.installSeedRefreshSnaps(c,
		seedRefreshSnap{name: "kernel", snapID: "kernel-id", snapType: "kernel"},
		seedRefreshSnap{name: "core18", snapID: "core18-snap-id", snapType: "base"},
		seedRefreshSnap{name: "content-provider", snapID: "content-provider-id", snapType: "app", base: "core18"},
	)

	chg := s.state.NewChange("refresh-snap", "...")
	chg.AddTask(s.state.NewTask("create-recovery-system", "Create recovery system"))

	other := s.state.NewTask("same-change-update", "same-change update for content-provider")
	other.Set("snap-setup", &snapstate.SnapSetup{
		SideInfo: &snap.SideInfo{
			RealName: "content-provider",
			Revision: snap.R(11),
		},
	})
	chg.AddTask(other)

	goal := snapstate.StoreUpdateGoal(snapstate.StoreUpdate{InstanceName: "content-provider"})
	_, err := snapstate.UpdateOne(context.Background(), s.state, goal, nil, snapstate.Options{
		UserID:          s.user.ID,
		ConflictOptions: snapstate.ConflictOptions{FromChange: chg.ID(), DoNotIgnoreFromChangeInTaskConflictCheck: true},
	})
	c.Assert(err, FitsTypeOf, &snapstate.ChangeConflictError{})
	c.Assert(err, ErrorMatches, `snap "content-provider" has "refresh-snap" change in progress`)

	var conflErr *snapstate.ChangeConflictError
	c.Assert(errors.As(err, &conflErr), Equals, true)
	c.Check(conflErr.ChangeID, Equals, chg.ID())
}

func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshBlockedByOtherChanges(c *C) {
	_, restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{
		"kernel": "kernel",
		"base":   "core18",
	}, []string{"kernel", "core18"})
	defer restore()

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

	s.installSeedRefreshSnaps(c,
		seedRefreshSnap{name: "kernel", snapID: "kernel-id", snapType: "kernel"},
		seedRefreshSnap{name: "core18", snapID: "core18-snap-id", snapType: "base"},
	)

	chg := s.state.NewChange("unrelated", "...")
	chg.AddTask(s.state.NewTask("task", "..."))

	updates := []snapstate.StoreUpdate{{InstanceName: "kernel"}, {InstanceName: "core18"}}
	goal := snapstate.StoreUpdateGoal(updates...)

	_, _, err := snapstate.UpdateWithGoal(context.Background(), s.state, goal, nil, snapstate.Options{
		UserID: s.user.ID,
		Flags: snapstate.Flags{
			Transaction: client.TransactionPerSnap,
		},
	})
	c.Assert(err, ErrorMatches, `other changes in progress \(conflicting change "unrelated"\), change "seed refresh" not allowed until they are done`)
}

func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshDisabled(c *C) {
	restore := snapstate.MockRevisionDate(nil)
	defer restore()

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

	observed, restore := s.setupSeedRefreshUpdateTest(c, false, false, map[string]any{
		"kernel": "kernel",
		"base":   "core18",
	}, nil)
	defer restore()

	s.installSeedRefreshSnaps(c,
		seedRefreshSnap{name: "kernel", snapID: "kernel-id", snapType: "kernel"},
		seedRefreshSnap{name: "core18", snapID: "core18-snap-id", snapType: "base"},
		seedRefreshSnap{name: "some-app", snapID: "some-app-id", snapType: "app", base: "core18"},
	)

	uts, _ := runSeedRefreshUpdate(c, s.state, s.user.ID, []snapstate.StoreUpdate{
		{InstanceName: "kernel"},
		{InstanceName: "core18"},
		{InstanceName: "some-app"},
	})
	taskSetsBySnap, seedTS := parseSeedRefreshTaskSets(uts)

	baseTS := mustTaskSetForSnap(c, taskSetsBySnap, "core18")
	kernelTS := mustTaskSetForSnap(c, taskSetsBySnap, "kernel")
	appTS := mustTaskSetForSnap(c, taskSetsBySnap, "some-app")

	c.Assert(seedTS, IsNil)
	c.Check(observed.initial, HasLen, 0)
	c.Check(observed.prerequisites, HasLen, 0)
	c.Check(observed.evictions, HasLen, 0)

	lastBeforeLocalBase, err := baseTS.Edge(snapstate.LastBeforeLocalModificationsEdge)
	c.Assert(err, IsNil)
	lastBeforeLocalKernel, err := kernelTS.Edge(snapstate.LastBeforeLocalModificationsEdge)
	c.Assert(err, IsNil)

	// note: this asserts that we do not have the dependencies set up by the
	// seed-refresh before-local-modifications phase. this check doesn't imply total ordering.
	firstLocalModApp := firstTaskAfterLocalModifications(c, appTS)
	c.Check(firstLocalModApp.WaitTasks(), Not(testutil.Contains), lastBeforeLocalBase)
	c.Check(firstLocalModApp.WaitTasks(), Not(testutil.Contains), lastBeforeLocalKernel)
}

func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshNoSeedRefreshOption(c *C) {
	restore := snapstate.MockRevisionDate(nil)
	defer restore()

	observed, restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{
		"kernel": "kernel",
		"base":   "core18",
	}, []string{"kernel", "core18"})
	defer restore()

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

	s.installSeedRefreshSnaps(c,
		seedRefreshSnap{name: "kernel", snapID: "kernel-id", snapType: "kernel"},
		seedRefreshSnap{name: "core18", snapID: "core18-snap-id", snapType: "base"},
	)

	goal := snapstate.StoreUpdateGoal(
		snapstate.StoreUpdate{InstanceName: "kernel"},
		snapstate.StoreUpdate{InstanceName: "core18"},
	)
	_, uts, err := snapstate.UpdateWithGoal(context.Background(), s.state, goal, nil, snapstate.Options{
		UserID:        s.user.ID,
		NoSeedRefresh: true,
		Flags: snapstate.Flags{
			Transaction: client.TransactionPerSnap,
		},
	})
	c.Assert(err, IsNil)

	_, seedTS := parseSeedRefreshTaskSets(uts)
	c.Check(seedTS, IsNil)
	c.Check(observed.initial, HasLen, 0)
	c.Check(observed.prerequisites, HasLen, 0)
}
