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

/*
 * Copyright (C) 2015-2022 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 asserts_test

import (
	"fmt"
	"regexp"

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

	"github.com/snapcore/snapd/asserts"
	"github.com/snapcore/snapd/metautil"
	"github.com/snapcore/snapd/snap"
	"github.com/snapcore/snapd/testutil"
)

type attrMatcherSuite struct {
	testutil.BaseTest
}

var _ = Suite(&attrMatcherSuite{})

func vals(yml string) map[string]any {
	var vs map[string]any
	err := yaml.Unmarshal([]byte(yml), &vs)
	if err != nil {
		panic(err)
	}
	v, err := metautil.NormalizeValue(vs)
	if err != nil {
		panic(err)
	}
	return v.(map[string]any)
}

func (s *attrMatcherSuite) SetUpTest(c *C) {
	s.BaseTest.SetUpTest(c)
	s.BaseTest.AddCleanup(snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {}))
}

func (s *attrMatcherSuite) TestSimple(c *C) {
	m, err := asserts.ParseHeaders([]byte(`attrs:
  foo: FOO
  bar: BAR`))
	c.Assert(err, IsNil)

	domatch, err := asserts.CompileAttrMatcher(m["attrs"].(map[string]any), nil, nil)
	c.Assert(err, IsNil)

	values := map[string]any{
		"foo": "FOO",
		"bar": "BAR",
		"baz": "BAZ",
	}
	err = domatch(values, nil)
	c.Check(err, IsNil)

	values = map[string]any{
		"foo": "FOO",
		"bar": "BAZ",
		"baz": "BAZ",
	}
	err = domatch(values, nil)
	c.Check(err, ErrorMatches, `field "bar" value "BAZ" does not match \^\(BAR\)\$`)

	values = map[string]any{
		"foo": "FOO",
		"baz": "BAZ",
	}
	err = domatch(values, nil)
	c.Check(err, ErrorMatches, `field "bar" has constraints but is unset`)
}

func (s *attrMatcherSuite) TestSimpleAnchorsVsRegexpAlt(c *C) {
	m, err := asserts.ParseHeaders([]byte(`attrs:
  bar: BAR|BAZ`))
	c.Assert(err, IsNil)

	domatch, err := asserts.CompileAttrMatcher(m["attrs"].(map[string]any), nil, nil)
	c.Assert(err, IsNil)

	values := map[string]any{
		"bar": "BAR",
	}
	err = domatch(values, nil)
	c.Check(err, IsNil)

	values = map[string]any{
		"bar": "BARR",
	}
	err = domatch(values, nil)
	c.Check(err, ErrorMatches, `field "bar" value "BARR" does not match \^\(BAR|BAZ\)\$`)

	values = map[string]any{
		"bar": "BBAZ",
	}
	err = domatch(values, nil)
	c.Check(err, ErrorMatches, `field "bar" value "BAZZ" does not match \^\(BAR|BAZ\)\$`)

	values = map[string]any{
		"bar": "BABAZ",
	}
	err = domatch(values, nil)
	c.Check(err, ErrorMatches, `field "bar" value "BABAZ" does not match \^\(BAR|BAZ\)\$`)

	values = map[string]any{
		"bar": "BARAZ",
	}
	err = domatch(values, nil)
	c.Check(err, ErrorMatches, `field "bar" value "BARAZ" does not match \^\(BAR|BAZ\)\$`)
}

func (s *attrMatcherSuite) TestNested(c *C) {
	m, err := asserts.ParseHeaders([]byte(`attrs:
  foo: FOO
  bar:
    bar1: BAR1
    bar2: BAR2`))
	c.Assert(err, IsNil)

	domatch, err := asserts.CompileAttrMatcher(m["attrs"].(map[string]any), nil, nil)
	c.Assert(err, IsNil)

	err = domatch(vals(`
foo: FOO
bar:
  bar1: BAR1
  bar2: BAR2
  bar3: BAR3
baz: BAZ
`), nil)
	c.Check(err, IsNil)

	err = domatch(vals(`
foo: FOO
bar: BAZ
baz: BAZ
`), nil)
	c.Check(err, ErrorMatches, `field "bar" must be a map`)

	err = domatch(vals(`
foo: FOO
bar:
  bar1: BAR1
  bar2: BAR22
  bar3: BAR3
baz: BAZ
`), nil)
	c.Check(err, ErrorMatches, `field "bar\.bar2" value "BAR22" does not match \^\(BAR2\)\$`)

	err = domatch(vals(`
foo: FOO
bar:
  bar1: BAR1
  bar2:
    bar22: true
  bar3: BAR3
baz: BAZ
`), nil)
	c.Check(err, ErrorMatches, `field "bar\.bar2" must be a scalar or list`)
}

func (s *attrMatcherSuite) TestAlternative(c *C) {
	m, err := asserts.ParseHeaders([]byte(`attrs:
  -
    foo: FOO
    bar: BAR
  -
    foo: FOO
    bar: BAZ`))
	c.Assert(err, IsNil)

	domatch, err := asserts.CompileAttrMatcher(m["attrs"], nil, nil)
	c.Assert(err, IsNil)

	values := map[string]any{
		"foo": "FOO",
		"bar": "BAR",
		"baz": "BAZ",
	}
	err = domatch(values, nil)
	c.Check(err, IsNil)

	values = map[string]any{
		"foo": "FOO",
		"bar": "BAZ",
		"baz": "BAZ",
	}
	err = domatch(values, nil)
	c.Check(err, IsNil)

	values = map[string]any{
		"foo": "FOO",
		"bar": "BARR",
		"baz": "BAR",
	}
	err = domatch(values, nil)
	c.Check(err, ErrorMatches, `no alternative matches: field "bar" value "BARR" does not match \^\(BAR\)\$`)
}

func (s *attrMatcherSuite) TestNestedAlternative(c *C) {
	m, err := asserts.ParseHeaders([]byte(`attrs:
  foo: FOO
  bar:
    bar1: BAR1
    bar2:
      - BAR2
      - BAR22`))
	c.Assert(err, IsNil)

	domatch, err := asserts.CompileAttrMatcher(m["attrs"].(map[string]any), nil, nil)
	c.Assert(err, IsNil)

	err = domatch(vals(`
foo: FOO
bar:
  bar1: BAR1
  bar2: BAR2
`), nil)
	c.Check(err, IsNil)

	err = domatch(vals(`
foo: FOO
bar:
  bar1: BAR1
  bar2: BAR22
`), nil)
	c.Check(err, IsNil)

	err = domatch(vals(`
foo: FOO
bar:
  bar1: BAR1
  bar2: BAR3
`), nil)
	c.Check(err, ErrorMatches, `no alternative for field "bar\.bar2" matches: field "bar\.bar2" value "BAR3" does not match \^\(BAR2\)\$`)
}

func (s *attrMatcherSuite) TestAlternativeMatchingStringList(c *C) {
	toMatch := vals(`
write:
 - /var/tmp
 - /var/lib/snapd/snapshots
`)
	m, err := asserts.ParseHeaders([]byte(`attrs:
  write: /var/(tmp|lib/snapd/snapshots)`))
	c.Assert(err, IsNil)

	domatch, err := asserts.CompileAttrMatcher(m["attrs"].(map[string]any), nil, nil)
	c.Assert(err, IsNil)

	err = domatch(toMatch, nil)
	c.Check(err, IsNil)

	m, err = asserts.ParseHeaders([]byte(`attrs:
  write:
    - /var/tmp
    - /var/lib/snapd/snapshots`))
	c.Assert(err, IsNil)

	domatchLst, err := asserts.CompileAttrMatcher(m["attrs"].(map[string]any), nil, nil)
	c.Assert(err, IsNil)

	err = domatchLst(toMatch, nil)
	c.Check(err, IsNil)
}

func (s *attrMatcherSuite) TestAlternativeMatchingComplex(c *C) {
	toMatch := vals(`
mnt: [{what: "/dev/x*", where: "/foo/*", options: ["rw", "nodev"]}, {what: "/bar/*", where: "/baz/*", options: ["rw", "bind"]}]
`)

	m, err := asserts.ParseHeaders([]byte(`attrs:
  mnt:
    -
      what: /(bar/|dev/x)\*
      where: /(foo|baz)/\*
      options: rw|bind|nodev`))
	c.Assert(err, IsNil)

	domatch, err := asserts.CompileAttrMatcher(m["attrs"].(map[string]any), nil, nil)
	c.Assert(err, IsNil)

	err = domatch(toMatch, nil)
	c.Check(err, IsNil)

	m, err = asserts.ParseHeaders([]byte(`attrs:
  mnt:
    -
      what: /dev/x\*
      where: /foo/\*
      options:
        - nodev
        - rw
    -
      what: /bar/\*
      where: /baz/\*
      options:
        - rw
        - bind`))
	c.Assert(err, IsNil)

	domatchExtensive, err := asserts.CompileAttrMatcher(m["attrs"].(map[string]any), nil, nil)
	c.Assert(err, IsNil)

	err = domatchExtensive(toMatch, nil)
	c.Check(err, IsNil)

	// not matching case
	m, err = asserts.ParseHeaders([]byte(`attrs:
  mnt:
    -
      what: /dev/x\*
      where: /foo/\*
      options:
        - rw
    -
      what: /bar/\*
      where: /baz/\*
      options:
        - rw
        - bind`))
	c.Assert(err, IsNil)

	domatchExtensiveNoMatch, err := asserts.CompileAttrMatcher(m["attrs"].(map[string]any), nil, nil)
	c.Assert(err, IsNil)

	err = domatchExtensiveNoMatch(toMatch, nil)
	c.Check(err, ErrorMatches, `no alternative for field "mnt\.0" matches: no alternative for field "mnt\.0.options\.1" matches:.*`)
}

func (s *attrMatcherSuite) TestOtherScalars(c *C) {
	m, err := asserts.ParseHeaders([]byte(`attrs:
  foo: 1
  bar: true`))
	c.Assert(err, IsNil)

	domatch, err := asserts.CompileAttrMatcher(m["attrs"].(map[string]any), nil, nil)
	c.Assert(err, IsNil)

	err = domatch(vals(`
foo: 1
bar: true
`), nil)
	c.Check(err, IsNil)

	values := map[string]any{
		"foo": int64(1),
		"bar": true,
	}
	err = domatch(values, nil)
	c.Check(err, IsNil)
}

func (s *attrMatcherSuite) TestCompileErrors(c *C) {
	_, err := asserts.CompileAttrMatcher(1, nil, nil)
	c.Check(err, ErrorMatches, `top constraint must be a key-value map, regexp or a list of alternative constraints: 1`)

	_, err = asserts.CompileAttrMatcher(map[string]any{
		"foo": 1,
	}, nil, nil)
	c.Check(err, ErrorMatches, `constraint "foo" must be a key-value map, regexp or a list of alternative constraints: 1`)

	_, err = asserts.CompileAttrMatcher(map[string]any{
		"foo": "[",
	}, nil, nil)
	c.Check(err, ErrorMatches, `cannot compile "foo" constraint "\[": error parsing regexp:.*`)

	_, err = asserts.CompileAttrMatcher(map[string]any{
		"foo": []any{"foo", "["},
	}, nil, nil)
	c.Check(err, ErrorMatches, `cannot compile "foo/alt#2/" constraint "\[": error parsing regexp:.*`)

	_, err = asserts.CompileAttrMatcher(map[string]any{
		"foo": []any{"foo", []any{"bar", "baz"}},
	}, nil, nil)
	c.Check(err, ErrorMatches, `cannot nest alternative constraints directly at "foo/alt#2/"`)

	_, err = asserts.CompileAttrMatcher("FOO", nil, nil)
	c.Check(err, ErrorMatches, `first level of non alternative constraints must be a set of key-value constraints`)

	_, err = asserts.CompileAttrMatcher([]any{"FOO"}, nil, nil)
	c.Check(err, ErrorMatches, `first level of non alternative constraints must be a set of key-value constraints`)

	_, err = asserts.CompileAttrMatcher(map[string]any{
		"foo": "$FOO()",
	}, nil, nil)
	c.Check(err, ErrorMatches, `cannot compile "foo" constraint "\$FOO\(\)": no \$OP\(\) or \$REF constraints supported`)

	wrongDollarConstraints := []string{
		"$",
		"$FOO(a)",
		"$SLOT",
		"$SLOT()",
		"$SLOT(x,y)",
		"$SLOT(x,y,z)",
		"$SLOT_COMPAT",
		"$SLOT_COMPAT()",
		"$SLOT_COMPAT(x,y)",
		"$SLOT_COMPAT(x,y,z)",
	}

	for _, wrong := range wrongDollarConstraints {
		_, err := asserts.CompileAttrMatcher(map[string]any{
			"foo": wrong,
		}, []string{"SLOT", "SLOT_COMPAT", "OP"}, []string{"PLUG_PUBLISHER_ID"})
		switch wrong {
		case "$SLOT(x,y)":
			c.Check(err, ErrorMatches, fmt.Sprintf(`cannot compile "foo" constraint "%s": \$SLOT\(\) constraint expects 1 argument`, regexp.QuoteMeta(wrong)))
		case "$SLOT_COMPAT(x,y)":
			c.Check(err, ErrorMatches, fmt.Sprintf(`cannot compile "foo" constraint "%s": \$SLOT_COMPAT\(\) constraint expects 1 argument`, regexp.QuoteMeta(wrong)))
		default:
			c.Check(err, ErrorMatches, fmt.Sprintf(`cannot compile "foo" constraint "%s": not a valid \$SLOT\(\)/\$SLOT_COMPAT\(\)/\$OP\(\)/\$PLUG_PUBLISHER_ID constraint`, regexp.QuoteMeta(wrong)))
		}
	}
}

func (s *attrMatcherSuite) TestMatchingListsSimple(c *C) {
	m, err := asserts.ParseHeaders([]byte(`attrs:
  foo: /foo/.*`))
	c.Assert(err, IsNil)

	domatch, err := asserts.CompileAttrMatcher(m["attrs"].(map[string]any), nil, nil)
	c.Assert(err, IsNil)

	err = domatch(vals(`
foo: ["/foo/x", "/foo/y"]
`), nil)
	c.Check(err, IsNil)

	err = domatch(vals(`
foo: ["/foo/x", "/foo"]
`), nil)
	c.Check(err, ErrorMatches, `field "foo\.1" value "/foo" does not match \^\(/foo/\.\*\)\$`)
}

func (s *attrMatcherSuite) TestMissingCheck(c *C) {
	m, err := asserts.ParseHeaders([]byte(`attrs:
  foo: $MISSING`))
	c.Assert(err, IsNil)

	domatch, err := asserts.CompileAttrMatcher(m["attrs"].(map[string]any), nil, nil)
	c.Assert(err, IsNil)

	err = domatch(vals(`
bar: baz
`), nil)
	c.Check(err, IsNil)

	err = domatch(vals(`
foo: ["x"]
`), nil)
	c.Check(err, ErrorMatches, `field "foo" is constrained to be missing but is set`)
}

func (s *attrMatcherSuite) TestEvalCheck(c *C) {
	// TODO: consider rewriting once we have $WITHIN
	m, err := asserts.ParseHeaders([]byte(`attrs:
  foo: $SLOT(foo)
  bar: $PLUG(bar.baz)`))
	c.Assert(err, IsNil)

	domatch, err := asserts.CompileAttrMatcher(m["attrs"].(map[string]any), []string{"SLOT", "PLUG"}, []string{"PLUG_PUBLISHER_ID"})
	c.Assert(err, IsNil)

	err = domatch(vals(`
foo: foo
bar: bar
`), nil)
	c.Check(err, ErrorMatches, `field "(foo|bar)" cannot be matched without context`)

	calls := make(map[[2]string]bool)
	comp1 := func(op string, arg string) (any, error) {
		calls[[2]string{op, arg}] = true
		return arg, nil
	}

	err = domatch(vals(`
foo: foo
bar: bar.baz
`), testEvalAttr{comp: comp1})
	c.Check(err, IsNil)

	c.Check(calls, DeepEquals, map[[2]string]bool{
		{"slot", "foo"}:     true,
		{"plug", "bar.baz"}: true,
	})

	comp2 := func(op string, arg string) (any, error) {
		if op == "plug" {
			return nil, fmt.Errorf("boom")
		}
		return arg, nil
	}

	err = domatch(vals(`
foo: foo
bar: bar.baz
`), testEvalAttr{comp: comp2})
	c.Check(err, ErrorMatches, `field "bar" constraint \$PLUG\(bar\.baz\) cannot be evaluated: boom`)

	comp3 := func(op string, arg string) (any, error) {
		if op == "slot" {
			return "other-value", nil
		}
		return arg, nil
	}

	err = domatch(vals(`
foo: foo
bar: bar.baz
`), testEvalAttr{comp: comp3})
	c.Check(err, ErrorMatches, `field "foo" does not match \$SLOT\(foo\): foo != other-value`)
}

func (s *attrMatcherSuite) TestMatchingListsMap(c *C) {
	m, err := asserts.ParseHeaders([]byte(`attrs:
  foo:
    p: /foo/.*`))
	c.Assert(err, IsNil)

	domatch, err := asserts.CompileAttrMatcher(m["attrs"].(map[string]any), nil, nil)
	c.Assert(err, IsNil)

	err = domatch(vals(`
foo: [{p: "/foo/x"}, {p: "/foo/y"}]
`), nil)
	c.Check(err, IsNil)

	err = domatch(vals(`
foo: [{p: "zzz"}, {p: "/foo/y"}]
`), nil)
	c.Check(err, ErrorMatches, `field "foo\.0\.p" value "zzz" does not match \^\(/foo/\.\*\)\$`)
}

type deviceScopeConstraintSuite struct {
	testutil.BaseTest
}

var _ = Suite(&deviceScopeConstraintSuite{})

func (s *deviceScopeConstraintSuite) TestCompile(c *C) {
	tests := []struct {
		m   map[string]any
		exp *asserts.DeviceScopeConstraint
		err string
	}{
		{m: nil, exp: nil},
		{m: map[string]any{"on-store": []any{"foo", "bar"}}, exp: &asserts.DeviceScopeConstraint{Store: []string{"foo", "bar"}}},
		{m: map[string]any{"on-brand": []any{"foo", "bar"}}, exp: &asserts.DeviceScopeConstraint{Brand: []string{"foo", "bar"}}},
		{m: map[string]any{"on-model": []any{"foo/model1", "bar/model-2"}}, exp: &asserts.DeviceScopeConstraint{Model: []string{"foo/model1", "bar/model-2"}}},
		{
			m: map[string]any{
				"on-brand": []any{"foo", "bar"},
				"on-model": []any{"foo/model1", "bar/model-2"},
				"on-store": []any{"foo", "bar"},
			},
			exp: &asserts.DeviceScopeConstraint{
				Store: []string{"foo", "bar"},
				Brand: []string{"foo", "bar"},
				Model: []string{"foo/model1", "bar/model-2"},
			},
		},
		{m: map[string]any{"on-store": ""}, err: `on-store in constraint must be a list of strings`},
		{m: map[string]any{"on-brand": "foo"}, err: `on-brand in constraint must be a list of strings`},
		{m: map[string]any{"on-model": map[string]any{"brand": "x"}}, err: `on-model in constraint must be a list of strings`},
	}

	for _, t := range tests {
		dsc, err := asserts.CompileDeviceScopeConstraint(t.m, "constraint")
		if t.err == "" {
			c.Check(err, IsNil)
			c.Check(dsc, DeepEquals, t.exp)
		} else {
			c.Check(err, ErrorMatches, t.err)
			c.Check(dsc, IsNil)
		}
	}
}

func (s *deviceScopeConstraintSuite) TestValidOnStoreBrandModel(c *C) {
	tests := []struct {
		constr string
		value  string
		valid  bool
	}{
		{"on-store", "", false},
		{"on-store", "foo", true},
		{"on-store", "F_o-O88", true},
		{"on-store", "foo!", false},
		{"on-store", "foo.", false},
		{"on-store", "foo/", false},
		{"on-brand", "", false},
		// custom set brands (length 2-28)
		{"on-brand", "dwell", true},
		{"on-brand", "Dwell", false},
		{"on-brand", "dwell-88", true},
		{"on-brand", "dwell_88", false},
		{"on-brand", "dwell.88", false},
		{"on-brand", "dwell:88", false},
		{"on-brand", "dwell!88", false},
		{"on-brand", "a", false},
		{"on-brand", "ab", true},
		{"on-brand", "0123456789012345678901234567", true},
		// snappy id brands (fixed length 32)
		{"on-brand", "01234567890123456789012345678", false},
		{"on-brand", "012345678901234567890123456789", false},
		{"on-brand", "0123456789012345678901234567890", false},
		{"on-brand", "01234567890123456789012345678901", true},
		{"on-brand", "abcdefghijklmnopqrstuvwxyz678901", true},
		{"on-brand", "ABCDEFGHIJKLMNOPQRSTUVWCYZ678901", true},
		{"on-brand", "ABCDEFGHIJKLMNOPQRSTUVWCYZ678901X", false},
		{"on-brand", "ABCDEFGHIJKLMNOPQ!STUVWCYZ678901", false},
		{"on-brand", "ABCDEFGHIJKLMNOPQ_STUVWCYZ678901", false},
		{"on-brand", "ABCDEFGHIJKLMNOPQ-STUVWCYZ678901", false},
		{"on-model", "", false},
		{"on-model", "/", false},
		{"on-model", "dwell/dwell1", true},
		{"on-model", "dwell", false},
		{"on-model", "dwell/", false},
		{"on-model", "dwell//dwell1", false},
		{"on-model", "dwell/-dwell1", false},
		{"on-model", "dwell/dwell1-", false},
		{"on-model", "dwell/dwell1-23", true},
		{"on-model", "dwell/dwell1!", false},
		{"on-model", "dwell/dwe_ll1", false},
		{"on-model", "dwell/dwe.ll1", false},
	}

	check := func(constr, value string, valid bool) {
		cMap := map[string]any{
			constr: []any{value},
		}

		_, err := asserts.CompileDeviceScopeConstraint(cMap, "constraint")
		if valid {
			c.Check(err, IsNil, Commentf("%v", cMap))
		} else {
			c.Check(err, ErrorMatches, fmt.Sprintf(`%s in constraint contains an invalid element: %q`, constr, value), Commentf("%v", cMap))
		}
	}

	for _, t := range tests {
		check(t.constr, t.value, t.valid)

		if t.constr == "on-brand" {
			// reuse and double check all brands also in the context of on-model!

			check("on-model", t.value+"/foo", t.valid)
		}
	}
}

func (s *deviceScopeConstraintSuite) TestCheck(c *C) {
	a, err := asserts.Decode([]byte(`type: model
authority-id: my-brand
series: 16
brand-id: my-brand
model: my-model1
store: store1
architecture: armhf
kernel: krnl
gadget: gadget
timestamp: 2018-09-12T12:00:00Z
sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij

AXNpZw==`))
	c.Assert(err, IsNil)
	myModel1 := a.(*asserts.Model)

	a, err = asserts.Decode([]byte(`type: model
authority-id: my-brand-subbrand
series: 16
brand-id: my-brand-subbrand
model: my-model2
store: store2
architecture: armhf
kernel: krnl
gadget: gadget
timestamp: 2018-09-12T12:00:00Z
sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij

AXNpZw==`))
	c.Assert(err, IsNil)
	myModel2 := a.(*asserts.Model)

	a, err = asserts.Decode([]byte(`type: model
authority-id: my-brand
series: 16
brand-id: my-brand
model: my-model3
store: substore1
architecture: armhf
kernel: krnl
gadget: gadget
timestamp: 2018-09-12T12:00:00Z
sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij

AXNpZw==`))
	c.Assert(err, IsNil)
	myModel3 := a.(*asserts.Model)

	a, err = asserts.Decode([]byte(`type: store
store: substore1
authority-id: canonical
operator-id: canonical
friendly-stores:
  - a-store
  - store1
  - store2
timestamp: 2018-09-12T12:00:00Z
sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij

AXNpZw==`))
	c.Assert(err, IsNil)
	substore1 := a.(*asserts.Store)

	tests := []struct {
		m                 map[string]any
		model             *asserts.Model
		store             *asserts.Store
		useFriendlyStores bool
		err               string
	}{
		{m: map[string]any{"on-store": []any{"store1"}}, model: myModel1},
		{m: map[string]any{"on-store": []any{"a-store", "store1"}}, model: myModel1},
		{m: map[string]any{"on-store": []any{"store1"}}, model: myModel2, err: "on-store mismatch"},
		{m: map[string]any{"on-store": []any{"store1"}}, model: myModel3, store: substore1, useFriendlyStores: true},
		{m: map[string]any{"on-store": []any{"store1"}}, model: myModel3, store: substore1, useFriendlyStores: false, err: "on-store mismatch"},
		{m: map[string]any{"on-store": []any{"store1"}}, model: nil, err: `cannot match on-store/on-brand/on-model without model`},
		{m: map[string]any{"on-store": []any{"store1"}}, model: myModel2, store: substore1, err: `store assertion and model store must match`, useFriendlyStores: true},
		{m: map[string]any{"on-store": []any{"other-store"}}, model: myModel3, store: substore1, err: "on-store mismatch"},
		{m: map[string]any{"on-brand": []any{"my-brand"}}, model: myModel1},
		{m: map[string]any{"on-brand": []any{"my-brand", "my-brand-subbrand"}}, model: myModel2},
		{m: map[string]any{"on-brand": []any{"other-brand"}}, model: myModel2, err: "on-brand mismatch"},
		{m: map[string]any{"on-model": []any{"my-brand/my-model1"}}, model: myModel1},
		{m: map[string]any{"on-model": []any{"my-brand/other-model"}}, model: myModel1, err: "on-model mismatch"},
		{m: map[string]any{"on-model": []any{"my-brand/my-model", "my-brand-subbrand/my-model2", "other-brand/other-model"}}, model: myModel2},
		{
			m: map[string]any{
				"on-store": []any{"store2"},
				"on-brand": []any{"my-brand", "my-brand-subbrand"},
				"on-model": []any{"my-brand/my-model3", "my-brand-subbrand/my-model2"},
			},
			model: myModel2,
		}, {
			m: map[string]any{
				"on-store": []any{"store2"},
				"on-brand": []any{"my-brand", "my-brand-subbrand"},
				"on-model": []any{"my-brand/my-model3", "my-brand-subbrand/my-model2"},
			},
			model: myModel3, store: substore1,
			useFriendlyStores: true,
		}, {
			m: map[string]any{
				"on-store": []any{"other-store"},
				"on-brand": []any{"my-brand", "my-brand-subbrand"},
				"on-model": []any{"my-brand/my-model3", "my-brand-subbrand/my-model2"},
			},
			model: myModel3, store: substore1,
			useFriendlyStores: true,
			err:               "on-store mismatch",
		}, {
			m: map[string]any{
				"on-store": []any{"store2"},
				"on-brand": []any{"other-brand", "my-brand-subbrand"},
				"on-model": []any{"my-brand/my-model3", "my-brand-subbrand/my-model2"},
			},
			model: myModel3, store: substore1,
			useFriendlyStores: true,
			err:               "on-brand mismatch",
		}, {
			m: map[string]any{
				"on-store": []any{"store2"},
				"on-brand": []any{"my-brand", "my-brand-subbrand"},
				"on-model": []any{"my-brand/my-model1", "my-brand-subbrand/my-model2"},
			},
			model: myModel3, store: substore1,
			useFriendlyStores: true,
			err:               "on-model mismatch",
		}, {
			m: map[string]any{
				"on-store": []any{"store2"},
				"on-brand": []any{"my-brand", "my-brand-subbrand"},
				"on-model": []any{"my-brand/my-model1", "my-brand-subbrand/my-model2"},
			},
			model: myModel3, store: substore1,
			useFriendlyStores: false,
			err:               "on-store mismatch",
		},
	}

	for _, t := range tests {
		constr, err := asserts.CompileDeviceScopeConstraint(t.m, "constraint")
		c.Assert(err, IsNil)

		var opts *asserts.DeviceScopeConstraintCheckOptions
		if t.useFriendlyStores {
			opts = &asserts.DeviceScopeConstraintCheckOptions{
				UseFriendlyStores: true,
			}
		}
		err = constr.Check(t.model, t.store, opts)
		if t.err == "" {
			c.Check(err, IsNil, Commentf("%v", t.m))
		} else {
			c.Check(err, ErrorMatches, t.err)
		}
	}
}
