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

/*
 * Copyright (C) 2020 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 daemon_test

import (
	"fmt"
	"net/http"
	"net/url"
	"sort"
	"strings"

	"gopkg.in/check.v1"

	"github.com/snapcore/snapd/asserts"
	"github.com/snapcore/snapd/asserts/assertstest"
	"github.com/snapcore/snapd/asserts/snapasserts"
	"github.com/snapcore/snapd/daemon"
	"github.com/snapcore/snapd/overlord/assertstate"
	"github.com/snapcore/snapd/overlord/assertstate/assertstatetest"
	"github.com/snapcore/snapd/overlord/auth"
	"github.com/snapcore/snapd/overlord/snapstate"
	"github.com/snapcore/snapd/overlord/snapstate/snapstatetest"
	"github.com/snapcore/snapd/overlord/state"
	"github.com/snapcore/snapd/snap"
	"github.com/snapcore/snapd/snap/naming"
	"github.com/snapcore/snapd/testutil"
)

var _ = check.Suite(&apiValidationSetsSuite{})

type apiValidationSetsSuite struct {
	apiBaseSuite

	storeSigning              *assertstest.StoreStack
	dev1Signing               *assertstest.SigningDB
	dev1acct                  *asserts.Account
	acct1Key                  *asserts.AccountKey
	mockSeqFormingAssertionFn func(assertType *asserts.AssertionType, sequenceKey []string, sequence int, user *auth.UserState) (asserts.Assertion, error)
}

type byName []*snapasserts.InstalledSnap

func (b byName) Len() int      { return len(b) }
func (b byName) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
func (b byName) Less(i, j int) bool {
	return b[i].SnapName() < b[j].SnapName()
}

func (s *apiValidationSetsSuite) SetUpTest(c *check.C) {
	s.apiBaseSuite.SetUpTest(c)
	d := s.daemon(c)

	s.expectAuthenticatedAccess()

	restore := asserts.MockMaxSupportedFormat(asserts.ValidationSetType, 1)
	s.AddCleanup(restore)

	s.mockSeqFormingAssertionFn = nil

	s.storeSigning = assertstest.NewStoreStack("can0nical", nil)

	st := d.Overlord().State()
	st.Lock()
	snapstate.ReplaceStore(st, s)
	assertstatetest.AddMany(st, s.storeSigning.StoreAccountKey(""))
	st.Unlock()

	s.dev1acct = assertstest.NewAccount(s.storeSigning, "developer1", nil, "")
	c.Assert(s.storeSigning.Add(s.dev1acct), check.IsNil)

	// developer signing
	dev1PrivKey, _ := assertstest.GenerateKey(752)
	s.acct1Key = assertstest.NewAccountKey(s.storeSigning, s.dev1acct, nil, dev1PrivKey.PublicKey(), "")

	s.dev1Signing = assertstest.NewSigningDB(s.dev1acct.AccountID(), dev1PrivKey)
	c.Assert(s.storeSigning.Add(s.acct1Key), check.IsNil)

	d.Overlord().Loop()
	s.AddCleanup(func() { d.Overlord().Stop() })
}

func (s *apiValidationSetsSuite) mockValidationSetsTracking(st *state.State) {
	st.Set("validation-sets", map[string]any{
		fmt.Sprintf("%s/foo", s.dev1acct.AccountID()): map[string]any{
			"account-id": s.dev1acct.AccountID(),
			"name":       "foo",
			"mode":       assertstate.Enforce,
			"pinned-at":  9,
			// Current should equal pinned-at if pinned-at != 0 but let's check api_validate is robust
			"current": 99,
		},
		fmt.Sprintf("%s/baz", s.dev1acct.AccountID()): map[string]any{
			"account-id": s.dev1acct.AccountID(),
			"name":       "baz",
			"mode":       assertstate.Monitor,
			"pinned-at":  0,
			"current":    2,
		},
	})
}

func (s *apiValidationSetsSuite) mockAssert(c *check.C, name, sequence string) asserts.Assertion {
	snaps := []any{map[string]any{
		"id":       "yOqKhntON3vR7kwEbVPsILm7bUViPDzz",
		"name":     "snap-b",
		"presence": "required",
		"revision": "1",
	}}
	headers := map[string]any{
		"authority-id": s.dev1acct.AccountID(),
		"account-id":   s.dev1acct.AccountID(),
		"name":         name,
		"series":       "16",
		"sequence":     sequence,
		"revision":     "5",
		"timestamp":    "2030-11-06T09:16:26Z",
		"snaps":        snaps,
	}
	vs, err := s.dev1Signing.Sign(asserts.ValidationSetType, headers, nil, "")
	c.Assert(err, check.IsNil)
	return vs
}

func (s *apiValidationSetsSuite) SeqFormingAssertion(assertType *asserts.AssertionType, sequenceKey []string, sequence int, user *auth.UserState) (asserts.Assertion, error) {
	s.pokeStateLock()
	return s.mockSeqFormingAssertionFn(assertType, sequenceKey, sequence, user)
}

func (s *apiValidationSetsSuite) TestQueryValidationSetsErrors(c *check.C) {
	s.mockSeqFormingAssertionFn = func(assertType *asserts.AssertionType, sequenceKey []string, sequence int, user *auth.UserState) (asserts.Assertion, error) {
		return nil, &asserts.NotFoundError{
			Type: assertType,
		}
	}

	st := s.d.Overlord().State()
	st.Lock()
	s.mockValidationSetsTracking(st)
	st.Unlock()

	for i, tc := range []struct {
		validationSet string
		// sequence is normally an int, use string for passing invalid ones.
		sequence string
		message  string
		status   int
	}{
		{
			validationSet: "abc/Xfoo",
			message:       `invalid name "Xfoo"`,
			status:        400,
		},
		{
			validationSet: "Xfoo/bar",
			message:       `invalid account ID "Xfoo"`,
			status:        400,
		},
		{
			validationSet: "foo/foo",
			message:       "validation set not found",
			status:        404,
		},
		{
			validationSet: "foo/bar",
			sequence:      "1999",
			message:       "validation set not found",
			status:        404,
		},
		{
			validationSet: "foo/bar",
			sequence:      "x",
			message:       "invalid sequence argument",
			status:        400,
		},
		{
			validationSet: "foo/bar",
			sequence:      "-2",
			message:       "invalid sequence argument: -2",
			status:        400,
		},
	} {
		q := url.Values{}
		if tc.sequence != "" {
			q.Set("sequence", tc.sequence)
		}
		req, err := http.NewRequest("GET", fmt.Sprintf("/v2/validation-sets/%s?%s", tc.validationSet, q.Encode()), nil)
		c.Assert(err, check.IsNil)
		rspe := s.errorReq(c, req, nil, actionIsExpected)
		c.Check(rspe.Status, check.Equals, tc.status, check.Commentf("case #%d", i))
		c.Check(rspe.Message, check.Matches, tc.message)
	}
}

func (s *apiValidationSetsSuite) TestGetValidationSetsNone(c *check.C) {
	req, err := http.NewRequest("GET", "/v2/validation-sets", nil)
	c.Assert(err, check.IsNil)

	rsp := s.syncReq(c, req, nil, actionIsExpected)
	c.Assert(rsp.Status, check.Equals, 200)
	res := rsp.Result.([]daemon.ValidationSetResult)
	c.Check(res, check.HasLen, 0)
}

func (s *apiValidationSetsSuite) TestListValidationSets(c *check.C) {
	st := s.d.Overlord().State()
	st.Lock()
	s.mockValidationSetsTracking(st)
	assertstatetest.AddMany(st, s.dev1acct, s.acct1Key)
	as := s.mockAssert(c, "foo", "9")
	err := assertstate.Add(st, as)
	c.Check(err, check.IsNil)
	as = s.mockAssert(c, "baz", "2")
	err = assertstate.Add(st, as)
	st.Unlock()
	c.Assert(err, check.IsNil)

	req, err := http.NewRequest("GET", "/v2/validation-sets", nil)
	c.Assert(err, check.IsNil)
	rsp := s.syncReq(c, req, nil, actionIsExpected)
	c.Assert(rsp.Status, check.Equals, 200)
	res := rsp.Result.([]daemon.ValidationSetResult)
	c.Check(res, check.DeepEquals, []daemon.ValidationSetResult{
		{
			AccountID: s.dev1acct.AccountID(),
			Name:      "baz",
			Mode:      "monitor",
			Sequence:  2,
			Valid:     false,
		},
		{
			AccountID: s.dev1acct.AccountID(),
			Name:      "foo",
			PinnedAt:  9,
			Mode:      "enforce",
			Sequence:  9,
			Valid:     false,
		},
	})
}

func (s *apiValidationSetsSuite) TestGetValidationSetOne(c *check.C) {
	s.mockSeqFormingAssertionFn = func(assertType *asserts.AssertionType, sequenceKey []string, sequence int, user *auth.UserState) (asserts.Assertion, error) {
		return nil, &asserts.NotFoundError{
			Type: assertType,
		}
	}

	st := s.d.Overlord().State()
	st.Lock()
	as := s.mockAssert(c, "baz", "2")
	assertstatetest.AddMany(st, s.dev1acct, s.acct1Key, as)
	s.mockValidationSetsTracking(st)
	st.Unlock()

	req, err := http.NewRequest("GET", fmt.Sprintf("/v2/validation-sets/%s/baz", s.dev1acct.AccountID()), nil)
	c.Assert(err, check.IsNil)

	rsp := s.syncReq(c, req, nil, actionIsExpected)
	c.Assert(rsp.Status, check.Equals, 200)
	res := rsp.Result.(daemon.ValidationSetResult)
	c.Check(res, check.DeepEquals, daemon.ValidationSetResult{
		AccountID: s.dev1acct.AccountID(),
		Name:      "baz",
		Mode:      "monitor",
		Sequence:  2,
		Valid:     false,
	})
}

func (s *apiValidationSetsSuite) TestGetValidationSetPinned(c *check.C) {
	q := url.Values{}
	q.Set("sequence", "9")
	req, err := http.NewRequest("GET", fmt.Sprintf("/v2/validation-sets/%s/foo?%s", s.dev1acct.AccountID(), q.Encode()), nil)
	c.Assert(err, check.IsNil)

	st := s.d.Overlord().State()
	st.Lock()
	as := s.mockAssert(c, "foo", "9")
	assertstatetest.AddMany(st, s.dev1acct, s.acct1Key, as)
	s.mockValidationSetsTracking(st)
	st.Unlock()
	c.Assert(err, check.IsNil)

	rsp := s.syncReq(c, req, nil, actionIsExpected)
	c.Assert(rsp.Status, check.Equals, 200)
	res := rsp.Result.(daemon.ValidationSetResult)
	c.Check(res, check.DeepEquals, daemon.ValidationSetResult{
		AccountID: s.dev1acct.AccountID(),
		Name:      "foo",
		PinnedAt:  9,
		Mode:      "enforce",
		Sequence:  9,
		Valid:     false,
	})
}

func (s *apiValidationSetsSuite) TestGetValidationSetNotFound(c *check.C) {
	s.mockSeqFormingAssertionFn = func(assertType *asserts.AssertionType, sequenceKey []string, sequence int, user *auth.UserState) (asserts.Assertion, error) {
		return nil, &asserts.NotFoundError{
			Type: assertType,
		}
	}

	req, err := http.NewRequest("GET", "/v2/validation-sets/foo/other", nil)
	c.Assert(err, check.IsNil)

	st := s.d.Overlord().State()
	st.Lock()
	s.mockValidationSetsTracking(st)
	st.Unlock()

	rspe := s.errorReq(c, req, nil, actionIsExpected)
	c.Assert(rspe.Status, check.Equals, 404)
	c.Check(string(rspe.Kind), check.Equals, "validation-set-not-found")
	c.Check(rspe.Value, check.DeepEquals, map[string]any{
		"account-id": "foo",
		"name":       "other",
	})
}

var validationSetAssertion = []byte("type: validation-set\n" +
	"format: 1\n" +
	"authority-id: foo\n" +
	"account-id: foo\n" +
	"name: other\n" +
	"sequence: 2\n" +
	"revision: 5\n" +
	"series: 16\n" +
	"snaps:\n" +
	"  -\n" +
	"    id: yOqKhntON3vR7kwEbVPsILm7bUViPDzz\n" +
	"    name: snap-b\n" +
	"    presence: required\n" +
	"    revision: 1\n" +
	"timestamp: 2020-11-06T09:16:26Z\n" +
	"sign-key-sha3-384: 7bbncP0c4RcufwReeiylCe0S7IMCn-tHLNSCgeOVmV3K-7_MzpAHgJDYeOjldefE\n\n" +
	"AXNpZw==")

func (s *apiValidationSetsSuite) TestGetValidationSetLatestFromRemote(c *check.C) {
	s.mockSeqFormingAssertionFn = func(assertType *asserts.AssertionType, sequenceKey []string, sequence int, user *auth.UserState) (asserts.Assertion, error) {
		c.Assert(assertType, check.NotNil)
		c.Assert(assertType.Name, check.Equals, "validation-set")
		// no sequence number element, querying the latest
		c.Assert(sequenceKey, check.DeepEquals, []string{"16", "foo", "other"})
		c.Assert(sequence, check.Equals, 0)
		as, err := asserts.Decode(validationSetAssertion)
		c.Assert(err, check.IsNil)
		// validity
		c.Assert(as.Type().Name, check.Equals, "validation-set")
		return as, nil
	}

	restore := daemon.MockCheckInstalledSnaps(func(vsets *snapasserts.ValidationSets, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) error {
		c.Assert(vsets, check.NotNil)
		sort.Sort(byName(snaps))
		c.Assert(snaps, check.DeepEquals, []*snapasserts.InstalledSnap{
			{
				SnapRef:  naming.NewSnapRef("snap-a", "snapaid"),
				Revision: snap.R(2),
			},
			{
				SnapRef:  naming.NewSnapRef("snap-b", "snapbid"),
				Revision: snap.R(4),
			},
		})
		c.Assert(ignoreValidation, check.IsNil)
		// nil indicates successful validation
		return nil
	})
	defer restore()

	req, err := http.NewRequest("GET", "/v2/validation-sets/foo/other", nil)
	c.Assert(err, check.IsNil)

	st := s.d.Overlord().State()
	st.Lock()
	s.mockValidationSetsTracking(st)

	snapstate.Set(st, "snap-a", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{RealName: "snap-a", Revision: snap.R(2), SnapID: "snapaid"}}),
		Current:  snap.R(2),
	})
	snapstate.Set(st, "snap-b", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{RealName: "snap-b", Revision: snap.R(4), SnapID: "snapbid"}}),
		Current:  snap.R(4),
	})

	st.Unlock()

	rsp := s.syncReq(c, req, nil, actionIsExpected)
	c.Assert(rsp.Status, check.Equals, 200)
	res := rsp.Result.(daemon.ValidationSetResult)
	c.Check(res, check.DeepEquals, daemon.ValidationSetResult{
		AccountID: "foo",
		Name:      "other",
		Sequence:  2,
		Valid:     true,
	})
}

func (s *apiValidationSetsSuite) TestGetValidationSetLatestFromRemoteValidationFails(c *check.C) {
	s.mockSeqFormingAssertionFn = func(assertType *asserts.AssertionType, sequenceKey []string, sequence int, user *auth.UserState) (asserts.Assertion, error) {
		as, err := asserts.Decode(validationSetAssertion)
		c.Assert(err, check.IsNil)
		return as, nil
	}
	restore := daemon.MockCheckInstalledSnaps(func(vsets *snapasserts.ValidationSets, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) error {
		return &snapasserts.ValidationSetsValidationError{}
	})
	defer restore()

	req, err := http.NewRequest("GET", "/v2/validation-sets/foo/other", nil)
	c.Assert(err, check.IsNil)
	rsp := s.syncReq(c, req, nil, actionIsExpected)
	c.Assert(rsp.Status, check.Equals, 200)

	res := rsp.Result.(daemon.ValidationSetResult)
	c.Check(res, check.DeepEquals, daemon.ValidationSetResult{
		AccountID: "foo",
		Name:      "other",
		Sequence:  2,
		Valid:     false,
	})
}

func (s *apiValidationSetsSuite) TestGetValidationSetLatestFromRemoteRealValidation(c *check.C) {
	s.mockSeqFormingAssertionFn = func(assertType *asserts.AssertionType, sequenceKey []string, sequence int, user *auth.UserState) (asserts.Assertion, error) {
		as, err := asserts.Decode(validationSetAssertion)
		c.Assert(err, check.IsNil)
		return as, nil
	}

	st := s.d.Overlord().State()

	for _, tc := range []struct {
		revision                 snap.Revision
		expectedValidationStatus bool
	}{
		// required at revision 1 per validationSetAssertion, so it's valid
		{snap.R(1), true},
		// but revision 2 is not valid
		{snap.R(2), false},
	} {
		st.Lock()
		snapstate.Set(st, "snap-b", &snapstate.SnapState{
			Active:   true,
			Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{RealName: "snap-b", Revision: tc.revision, SnapID: "yOqKhntON3vR7kwEbVPsILm7bUViPDzz"}}),
			Current:  tc.revision,
		})
		st.Unlock()

		req, err := http.NewRequest("GET", "/v2/validation-sets/foo/other", nil)
		c.Assert(err, check.IsNil)
		rsp := s.syncReq(c, req, nil, actionIsExpected)
		c.Assert(rsp.Status, check.Equals, 200)

		res := rsp.Result.(daemon.ValidationSetResult)
		c.Check(res, check.DeepEquals, daemon.ValidationSetResult{
			AccountID: "foo",
			Name:      "other",
			Sequence:  2,
			Valid:     tc.expectedValidationStatus,
		})
	}
}

func (s *apiValidationSetsSuite) TestGetValidationSetSpecificSequenceFromRemote(c *check.C) {
	s.mockSeqFormingAssertionFn = func(assertType *asserts.AssertionType, sequenceKey []string, sequence int, user *auth.UserState) (asserts.Assertion, error) {
		c.Assert(assertType, check.NotNil)
		c.Assert(assertType.Name, check.Equals, "validation-set")
		c.Assert(sequenceKey, check.DeepEquals, []string{"16", "foo", "other"})
		c.Assert(sequence, check.Equals, 2)
		as, err := asserts.Decode(validationSetAssertion)
		c.Assert(err, check.IsNil)
		return as, nil
	}

	restore := daemon.MockCheckInstalledSnaps(func(vsets *snapasserts.ValidationSets, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) error {
		c.Assert(vsets, check.NotNil)
		sort.Sort(byName(snaps))
		c.Assert(snaps, check.DeepEquals, []*snapasserts.InstalledSnap{
			{
				SnapRef:  naming.NewSnapRef("snap-a", "snapaid"),
				Revision: snap.R(33),
			},
		})
		c.Assert(ignoreValidation, check.IsNil)
		// nil indicates successful validation
		return nil
	})
	defer restore()

	q := url.Values{}
	q.Set("sequence", "2")
	req, err := http.NewRequest("GET", "/v2/validation-sets/foo/other?"+q.Encode(), nil)
	c.Assert(err, check.IsNil)

	st := s.d.Overlord().State()
	st.Lock()
	s.mockValidationSetsTracking(st)

	snapstate.Set(st, "snap-a", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{RealName: "snap-a", Revision: snap.R(33), SnapID: "snapaid"}}),
		Current:  snap.R(33),
	})

	st.Unlock()

	rsp := s.syncReq(c, req, nil, actionIsExpected)
	c.Assert(rsp.Status, check.Equals, 200)
	res := rsp.Result.(daemon.ValidationSetResult)
	c.Check(res, check.DeepEquals, daemon.ValidationSetResult{
		AccountID: "foo",
		Name:      "other",
		Sequence:  2,
		Valid:     true,
	})
}

func (s *apiValidationSetsSuite) TestGetValidationSetFromRemoteFallbackToLocalAssertion(c *check.C) {
	s.mockSeqFormingAssertionFn = func(assertType *asserts.AssertionType, sequenceKey []string, sequence int, user *auth.UserState) (asserts.Assertion, error) {
		// not found in the store
		return nil, &asserts.NotFoundError{
			Type: assertType,
		}
	}
	restore := daemon.MockCheckInstalledSnaps(func(vsets *snapasserts.ValidationSets, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) error {
		// nil indicates successful validation
		return nil
	})
	defer restore()

	st := s.d.Overlord().State()
	st.Lock()
	// assertion available in the local db (from snap ack)
	vs := s.mockAssert(c, "bar", "2")
	assertstatetest.AddMany(st, s.dev1acct, s.acct1Key, vs)
	st.Unlock()

	q := url.Values{}
	q.Set("sequence", "2")
	req, err := http.NewRequest("GET", fmt.Sprintf("/v2/validation-sets/%s/bar?%s", s.dev1acct.AccountID(), q.Encode()), nil)
	c.Assert(err, check.IsNil)

	rsp := s.syncReq(c, req, nil, actionIsExpected)
	c.Assert(rsp.Status, check.Equals, 200)
	res := rsp.Result.(daemon.ValidationSetResult)
	c.Check(res, check.DeepEquals, daemon.ValidationSetResult{
		AccountID: s.dev1acct.AccountID(),
		Name:      "bar",
		Sequence:  2,
		Valid:     true,
	})
}

func (s *apiValidationSetsSuite) TestGetValidationSetPinnedNotFound(c *check.C) {
	s.mockSeqFormingAssertionFn = func(assertType *asserts.AssertionType, sequenceKey []string, sequence int, user *auth.UserState) (asserts.Assertion, error) {
		return nil, &asserts.NotFoundError{
			Type: assertType,
		}
	}

	q := url.Values{}
	q.Set("sequence", "333")
	req, err := http.NewRequest("GET", "/v2/validation-sets/foo/bar?"+q.Encode(), nil)
	c.Assert(err, check.IsNil)

	st := s.d.Overlord().State()
	st.Lock()
	s.mockValidationSetsTracking(st)
	st.Unlock()

	rspe := s.errorReq(c, req, nil, actionIsExpected)
	c.Assert(rspe.Status, check.Equals, 404)
	c.Check(string(rspe.Kind), check.Equals, "validation-set-not-found")
	c.Check(rspe.Value, check.DeepEquals, map[string]any{
		"account-id": "foo",
		"name":       "bar",
		"sequence":   333,
	})
}

func (s *apiValidationSetsSuite) TestApplyValidationSetMonitorModePinnedLocalOnly(c *check.C) {
	st := s.d.Overlord().State()
	st.Lock()
	s.mockValidationSetsTracking(st)
	assertstatetest.AddMany(st, s.dev1acct, s.acct1Key)
	as := s.mockAssert(c, "bar", "99")
	err := assertstate.Add(st, as)
	st.Unlock()
	c.Assert(err, check.IsNil)

	var called int
	restore := daemon.MockAssertstateMonitorValidationSet(func(st *state.State, accountID, name string, sequence, userID int) (*assertstate.ValidationSetTracking, error) {
		c.Assert(accountID, check.Equals, s.dev1acct.AccountID())
		c.Assert(name, check.Equals, "bar")
		c.Assert(sequence, check.Equals, 99)
		called++
		// Current should be the same as PinnedAt when PinnedAt != 0 but let's check api_validate is robust
		return &assertstate.ValidationSetTracking{AccountID: accountID, Name: name, PinnedAt: 99, Current: 99999}, nil
	})
	defer restore()

	restore = daemon.MockCheckInstalledSnaps(func(vsets *snapasserts.ValidationSets, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) error {
		// nil indicates successful validation
		return nil
	})
	defer restore()

	body := `{"action":"apply","mode":"monitor", "sequence":99}`
	req, err := http.NewRequest("POST", fmt.Sprintf("/v2/validation-sets/%s/bar", s.dev1acct.AccountID()), strings.NewReader(body))
	c.Assert(err, check.IsNil)

	rsp := s.syncReq(c, req, nil, actionIsExpected)
	c.Assert(rsp.Status, check.Equals, 200)
	res := rsp.Result.(daemon.ValidationSetResult)
	c.Check(res, check.DeepEquals, daemon.ValidationSetResult{
		AccountID: s.dev1acct.AccountID(),
		Name:      "bar",
		Mode:      "monitor",
		PinnedAt:  99,
		Sequence:  99,
		Valid:     true,
	})
	c.Check(called, check.Equals, 1)
}

func (s *apiValidationSetsSuite) TestApplyValidationSetMonitorModeError(c *check.C) {
	restore := daemon.MockAssertstateMonitorValidationSet(func(st *state.State, accountID, name string, sequence, userID int) (*assertstate.ValidationSetTracking, error) {
		return nil, fmt.Errorf("boom")
	})
	defer restore()

	body := `{"action":"apply","mode":"monitor"}`
	req, err := http.NewRequest("POST", fmt.Sprintf("/v2/validation-sets/%s/bar", s.dev1acct.AccountID()), strings.NewReader(body))
	c.Assert(err, check.IsNil)

	rspe := s.errorReq(c, req, nil, actionIsExpected)
	c.Assert(rspe.Status, check.Equals, 400)
	c.Check(rspe.Message, check.Equals, fmt.Sprintf(`cannot monitor validation set %s/bar: boom`, s.dev1acct.AccountID()))
}

func (s *apiValidationSetsSuite) TestForgetValidationSet(c *check.C) {
	st := s.d.Overlord().State()

	for i, sequence := range []int{0, 9} {
		st.Lock()
		s.mockValidationSetsTracking(st)
		st.Unlock()

		var body string
		if sequence != 0 {
			body = fmt.Sprintf(`{"action":"forget", "sequence":%d}`, sequence)
		} else {
			body = `{"action":"forget"}`
		}

		var tr assertstate.ValidationSetTracking

		st.Lock()
		// validity, it exists before removing
		err := assertstate.GetValidationSet(st, s.dev1acct.AccountID(), "foo", &tr)
		st.Unlock()
		c.Assert(err, check.IsNil)
		c.Check(tr.AccountID, check.Equals, s.dev1acct.AccountID())
		c.Check(tr.Name, check.Equals, "foo")

		req, err := http.NewRequest("POST", fmt.Sprintf("/v2/validation-sets/%s/foo", s.dev1acct.AccountID()), strings.NewReader(body))
		c.Assert(err, check.IsNil)
		rsp := s.syncReq(c, req, nil, actionIsExpected)
		c.Assert(rsp.Status, check.Equals, 200, check.Commentf("case #%d", i))

		// after forget it's removed
		st.Lock()
		err = assertstate.GetValidationSet(st, s.dev1acct.AccountID(), "foo", &tr)
		st.Unlock()
		c.Assert(err, testutil.ErrorIs, state.ErrNoState)

		// and forget again fails
		req, err = http.NewRequest("POST", fmt.Sprintf("/v2/validation-sets/%s/foo", s.dev1acct.AccountID()), strings.NewReader(body))
		c.Assert(err, check.IsNil)
		rspe := s.errorReq(c, req, nil, actionIsExpected)
		c.Assert(rspe.Status, check.Equals, 404, check.Commentf("case #%d", i))
	}
}

func (s *apiValidationSetsSuite) TestApplyValidationSetsErrors(c *check.C) {
	st := s.d.Overlord().State()
	st.Lock()
	s.mockValidationSetsTracking(st)
	st.Unlock()

	for i, tc := range []struct {
		validationSet string
		mode          string
		// sequence is normally an int, use string for passing invalid ones.
		sequence string
		message  string
		status   int
	}{
		{
			validationSet: "0/zzz",
			mode:          "monitor",
			message:       `invalid account ID "0"`,
			status:        400,
		},
		{
			validationSet: "Xfoo/bar",
			mode:          "monitor",
			message:       `invalid account ID "Xfoo"`,
			status:        400,
		},
		{
			validationSet: "foo/Xabc",
			mode:          "monitor",
			message:       `invalid name "Xabc"`,
			status:        400,
		},
		{
			validationSet: "foo/bar",
			sequence:      "x",
			message:       "cannot decode request body into validation set action: invalid character 'x' looking for beginning of value",
			status:        400,
		},
		{
			validationSet: "foo/bar",
			mode:          "bad",
			message:       `invalid mode "bad"`,
			status:        400,
		},
		{
			validationSet: "foo/bar",
			sequence:      "-1",
			mode:          "monitor",
			message:       `invalid sequence argument: -1`,
			status:        400,
		},
	} {
		var body string
		if tc.sequence != "" {
			body = fmt.Sprintf(`{"action":"apply","mode":"%s", "sequence":%s}`, tc.mode, tc.sequence)
		} else {
			body = fmt.Sprintf(`{"action":"apply","mode":"%s"}`, tc.mode)
		}
		req, err := http.NewRequest("POST", fmt.Sprintf("/v2/validation-sets/%s", tc.validationSet), strings.NewReader(body))
		c.Assert(err, check.IsNil)
		rspe := s.errorReq(c, req, nil, actionIsExpected)
		c.Check(rspe.Status, check.Equals, tc.status, check.Commentf("case #%d", i))
		c.Check(rspe.Message, check.Matches, tc.message)
	}
}

func (s *apiValidationSetsSuite) TestApplyValidationSetUnsupportedAction(c *check.C) {
	body := `{"action":"baz","mode":"monitor"}`

	req, err := http.NewRequest("POST", "/v2/validation-sets/foo/bar", strings.NewReader(body))
	c.Assert(err, check.IsNil)

	rspe := s.errorReq(c, req, nil, actionIsUnexpected)
	c.Check(rspe.Status, check.Equals, 400)
	c.Check(rspe.Message, check.Matches, `unsupported action "baz"`)
}

func (s *apiValidationSetsSuite) TestApplyValidationSetEnforceMode(c *check.C) {
	st := s.d.Overlord().State()
	st.Lock()
	defer st.Unlock()

	s.mockValidationSetsTracking(st)
	assertstatetest.AddMany(st, s.dev1acct, s.acct1Key)
	as := s.mockAssert(c, "bar", "99")
	err := assertstate.Add(st, as)
	c.Assert(err, check.IsNil)

	var called int
	restore := daemon.MockAssertstateFetchEnforceValidationSet(func(st *state.State, accountID, name string, sequence int, userID int, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) (*assertstate.ValidationSetTracking, error) {
		c.Check(ignoreValidation, check.HasLen, 0)
		c.Assert(accountID, check.Equals, s.dev1acct.AccountID())
		c.Assert(name, check.Equals, "bar")
		c.Assert(sequence, check.Equals, 0)
		c.Check(userID, check.Equals, 0)
		called++
		return &assertstate.ValidationSetTracking{AccountID: accountID, Name: name, Mode: assertstate.Enforce, Current: 99}, nil
	})
	defer restore()

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

	assertstatetest.AddMany(st, s.dev1acct, s.acct1Key)

	st.Unlock()
	defer st.Lock()
	body := `{"action":"apply","mode":"enforce"}`
	req, err := http.NewRequest("POST", fmt.Sprintf("/v2/validation-sets/%s/bar", s.dev1acct.AccountID()), strings.NewReader(body))
	c.Assert(err, check.IsNil)

	rsp := s.syncReq(c, req, nil, actionIsExpected)
	c.Assert(rsp.Status, check.Equals, 200)
	res := rsp.Result.(daemon.ValidationSetResult)
	c.Check(res, check.DeepEquals, daemon.ValidationSetResult{
		AccountID: s.dev1acct.AccountID(),
		Name:      "bar",
		Mode:      "enforce",
		Sequence:  99,
		Valid:     true,
	})
	c.Check(called, check.Equals, 1)
}

func (s *apiValidationSetsSuite) TestApplyValidationSetEnforceModeIgnoreValidationOK(c *check.C) {
	st := s.d.Overlord().State()
	st.Lock()
	defer st.Unlock()

	s.mockValidationSetsTracking(st)
	assertstatetest.AddMany(st, s.dev1acct, s.acct1Key)
	as := s.mockAssert(c, "bar", "99")
	err := assertstate.Add(st, as)
	c.Assert(err, check.IsNil)

	var called int
	restore := daemon.MockAssertstateFetchEnforceValidationSet(func(st *state.State, accountID, name string, sequence int, userID int, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) (*assertstate.ValidationSetTracking, error) {
		c.Check(ignoreValidation, check.DeepEquals, map[string]bool{"snap-b": true})
		c.Check(snaps, testutil.DeepUnsortedMatches, []*snapasserts.InstalledSnap{
			snapasserts.NewInstalledSnap("snap-b", "yOqKhntON3vR7kwEbVPsILm7bUViPDzz", snap.R("1"), nil)})
		c.Assert(accountID, check.Equals, s.dev1acct.AccountID())
		c.Assert(name, check.Equals, "bar")
		c.Assert(sequence, check.Equals, 0)
		c.Check(userID, check.Equals, 0)
		called++
		return &assertstate.ValidationSetTracking{AccountID: accountID, Name: name, Mode: assertstate.Enforce, Current: 99}, nil
	})
	defer restore()

	snapstate.Set(st, "snap-b", &snapstate.SnapState{
		Active:   true,
		Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{RealName: "snap-b", Revision: snap.R(1), SnapID: "yOqKhntON3vR7kwEbVPsILm7bUViPDzz"}}),
		Current:  snap.R(1),
		Flags:    snapstate.Flags{IgnoreValidation: true},
	})

	assertstatetest.AddMany(st, s.dev1acct, s.acct1Key)

	st.Unlock()
	defer st.Lock()
	body := `{"action":"apply","mode":"enforce"}`
	req, err := http.NewRequest("POST", fmt.Sprintf("/v2/validation-sets/%s/bar", s.dev1acct.AccountID()), strings.NewReader(body))
	c.Assert(err, check.IsNil)

	rsp := s.syncReq(c, req, nil, actionIsExpected)
	c.Assert(rsp.Status, check.Equals, 200)
	res := rsp.Result.(daemon.ValidationSetResult)
	c.Check(res, check.DeepEquals, daemon.ValidationSetResult{
		AccountID: s.dev1acct.AccountID(),
		Name:      "bar",
		Mode:      "enforce",
		Sequence:  99,
		Valid:     true,
	})
	c.Check(called, check.Equals, 1)
}

func (s *apiValidationSetsSuite) TestApplyValidationSetEnforceModeSpecificSequence(c *check.C) {
	st := s.d.Overlord().State()
	st.Lock()
	defer st.Unlock()

	s.mockValidationSetsTracking(st)
	assertstatetest.AddMany(st, s.dev1acct, s.acct1Key)
	as := s.mockAssert(c, "bar", "5")
	err := assertstate.Add(st, as)
	c.Assert(err, check.IsNil)

	var called int
	restore := daemon.MockAssertstateFetchEnforceValidationSet(func(st *state.State, accountID, name string, sequence int, userID int, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) (*assertstate.ValidationSetTracking, error) {
		c.Assert(accountID, check.Equals, s.dev1acct.AccountID())
		c.Assert(name, check.Equals, "bar")
		c.Assert(sequence, check.Equals, 5)
		c.Check(userID, check.Equals, 0)
		called++
		return &assertstate.ValidationSetTracking{AccountID: accountID, Name: name, Mode: assertstate.Enforce, PinnedAt: 5, Current: 5}, nil
	})
	defer restore()

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

	assertstatetest.AddMany(st, s.dev1acct, s.acct1Key)

	st.Unlock()
	defer st.Lock()
	body := `{"action":"apply","mode":"enforce","sequence":5}`
	req, err := http.NewRequest("POST", fmt.Sprintf("/v2/validation-sets/%s/bar", s.dev1acct.AccountID()), strings.NewReader(body))
	c.Assert(err, check.IsNil)

	rsp := s.syncReq(c, req, nil, actionIsExpected)
	c.Assert(rsp.Status, check.Equals, 200)
	res := rsp.Result.(daemon.ValidationSetResult)
	c.Check(res, check.DeepEquals, daemon.ValidationSetResult{
		AccountID: s.dev1acct.AccountID(),
		Name:      "bar",
		Mode:      "enforce",
		PinnedAt:  5,
		Sequence:  5,
		Valid:     true,
	})
	c.Check(called, check.Equals, 1)
}

func (s *apiValidationSetsSuite) TestApplyValidationSetEnforceModeError(c *check.C) {
	restore := daemon.MockAssertstateFetchEnforceValidationSet(func(st *state.State, accountID, name string, sequence int, userID int, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) (*assertstate.ValidationSetTracking, error) {
		return nil, fmt.Errorf("boom")
	})
	defer restore()

	st := s.d.Overlord().State()
	st.Lock()
	defer st.Unlock()

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

	assertstatetest.AddMany(st, s.dev1acct, s.acct1Key)

	st.Unlock()
	defer st.Lock()
	body := `{"action":"apply","mode":"enforce"}`
	req, err := http.NewRequest("POST", fmt.Sprintf("/v2/validation-sets/%s/bar", s.dev1acct.AccountID()), strings.NewReader(body))
	c.Assert(err, check.IsNil)

	rspe := s.errorReq(c, req, nil, actionIsExpected)
	c.Assert(rspe.Status, check.Equals, 400)
	c.Check(string(rspe.Message), check.Equals, "cannot enforce validation set: boom")
}
