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

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

import (
	"fmt"
	"regexp"

	. "gopkg.in/check.v1"

	"github.com/snapcore/snapd/snap/naming"
	"github.com/snapcore/snapd/testutil"
)

type ValidateSuite struct {
	testutil.BaseTest
}

var _ = Suite(&ValidateSuite{})

func (s *ValidateSuite) SetUpTest(c *C) {
	s.BaseTest.SetUpTest(c)
}

func (s *ValidateSuite) TearDownTest(c *C) {
	s.BaseTest.TearDownTest(c)
}

func (s *ValidateSuite) TestValidateName(c *C) {
	validNames := []string{
		"aa", "aaa", "aaaa",
		"a-a", "aa-a", "a-aa", "a-b-c",
		"a0", "a-0", "a-0a",
		"01game", "1-or-2",
		// a regexp stresser
		"u-94903713687486543234157734673284536758",
	}
	for _, name := range validNames {
		err := naming.ValidateSnap(name)
		c.Assert(err, IsNil)
	}
	invalidNames := []string{
		// name cannot be empty
		"",
		// too short (min 2 chars)
		"a",
		// names cannot be too long
		"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
		"xxxxxxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxx",
		"1111111111111111111111111111111111111111x",
		"x1111111111111111111111111111111111111111",
		"x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x",
		// a regexp stresser
		"u-9490371368748654323415773467328453675-",
		// dashes alone are not a name
		"-", "--",
		// double dashes in a name are not allowed
		"a--a",
		// name should not end with a dash
		"a-",
		// name cannot have any spaces in it
		"a ", " a", "a a",
		// a number alone is not a name
		"0", "123",
		// identifier must be plain ASCII
		"日本語", "한글", "ру́сский язы́к",
	}
	for _, name := range invalidNames {
		err := naming.ValidateSnap(name)
		c.Assert(err, ErrorMatches, `invalid snap name: ".*"`)
	}
}

func (s *ValidateSuite) TestValidateInstanceName(c *C) {
	validNames := []string{
		// plain names are also valid instance names
		"aa", "aaa", "aaaa",
		"a-a", "aa-a", "a-aa", "a-b-c",
		// snap instance
		"foo_bar",
		"foo_0123456789",
		"01game_0123456789",
		"foo_1", "foo_1234abcd",
	}
	for _, name := range validNames {
		err := naming.ValidateInstance(name)
		c.Assert(err, IsNil)
	}
	invalidNames := []string{
		// invalid names are also invalid instance names, just a few
		// samples
		"",
		"a",
		"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
		"xxxxxxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxx",
		"a--a",
		"a-",
		"a ", " a", "a a",
		"_",
		"ру́сский_язы́к",
	}
	for _, name := range invalidNames {
		err := naming.ValidateInstance(name)
		c.Assert(err, ErrorMatches, `invalid snap name: ".*"`)
	}
	invalidInstanceKeys := []string{
		// the snap names are valid, but instance keys are not
		"foo_", "foo_1-23", "foo_01234567890", "foo_123_456",
		"foo__bar",
	}
	for _, name := range invalidInstanceKeys {
		err := naming.ValidateInstance(name)
		c.Assert(err, ErrorMatches, `invalid instance key: ".*"`)
	}

}

func (s *ValidateSuite) TestValidateHookName(c *C) {
	validHooks := []string{
		"a",
		"aaa",
		"a-a",
		"aa-a",
		"a-aa",
		"a-b-c",
		"valid",
	}
	for _, hook := range validHooks {
		err := naming.ValidateHook(hook)
		c.Assert(err, IsNil)
	}
	invalidHooks := []string{
		"",
		"a a",
		"a--a",
		"-a",
		"a-",
		"0",
		"123",
		"123abc",
		"日本語",
	}
	for _, hook := range invalidHooks {
		err := naming.ValidateHook(hook)
		c.Assert(err, ErrorMatches, `invalid hook name: ".*"`)
	}
	// Regression test for https://bugs.launchpad.net/snapd/+bug/1638988
	c.Assert(naming.ValidateHook("connect-plug-i2c"), IsNil)
}

func (s *ValidateSuite) TestValidateAppName(c *C) {
	validAppNames := []string{
		"1", "a", "aa", "aaa", "aaaa", "Aa", "aA", "1a", "a1", "1-a", "a-1",
		"a-a", "aa-a", "a-aa", "a-b-c", "0a-a", "a-0a",
	}
	for _, name := range validAppNames {
		c.Check(naming.ValidateApp(name), IsNil)
	}
	invalidAppNames := []string{
		"", "-", "--", "a--a", "a-", "a ", " a", "a a", "日本語", "한글",
		"ру́сский язы́к", "ໄຂ່​ອີ​ສ​ເຕີ້", ":a", "a:", "a:a", "_a", "a_", "a_a",
	}
	for _, name := range invalidAppNames {
		err := naming.ValidateApp(name)
		c.Assert(err, ErrorMatches, `invalid app name: ".*"`)
	}
}

func (s *ValidateSuite) TestValidateAlias(c *C) {
	validAliases := []string{
		"a", "aa", "aaa", "aaaa",
		"a-a", "aa-a", "a-aa", "a-b-c",
		"a0", "a-0", "a-0a",
		"a_a", "aa_a", "a_aa", "a_b_c",
		"a0", "a_0", "a_0a",
		"01game", "1-or-2",
		"0.1game", "1_or_2",
	}
	for _, alias := range validAliases {
		err := naming.ValidateAlias(alias)
		c.Assert(err, IsNil)
	}
	invalidAliases := []string{
		"",
		"_foo",
		"-foo",
		".foo",
		"foo$",
	}
	for _, alias := range invalidAliases {
		err := naming.ValidateAlias(alias)
		c.Assert(err, ErrorMatches, `invalid alias name: ".*"`)
	}
}

func (s *ValidateSuite) TestValidateSocketName(c *C) {
	validNames := []string{
		"a", "aa", "aaa", "aaaa",
		"a-a", "aa-a", "a-aa", "a-b-c",
		"a0", "a-0", "a-0a",
		"01game", "1-or-2",
	}
	for _, name := range validNames {
		err := naming.ValidateSocket(name)
		c.Assert(err, IsNil)
	}
	invalidNames := []string{
		// name cannot be empty
		"",
		// dashes alone are not a name
		"-", "--",
		// double dashes in a name are not allowed
		"a--a",
		// name should not end with a dash
		"a-",
		// name cannot have any spaces in it
		"a ", " a", "a a",
		// a number alone is not a name
		"0", "123",
		// identifier must be plain ASCII
		"日本語", "한글", "ру́сский язы́к",
		// no null chars in the string are allowed
		"aa-a\000-b",
	}
	for _, name := range invalidNames {
		err := naming.ValidateSocket(name)
		c.Assert(err, ErrorMatches, `invalid socket name: ".*"`)
	}
}

func (s *ValidateSuite) TestValidateSlotPlugInterfaceName(c *C) {
	valid := []string{
		"a",
		"aaa",
		"a-a",
		"aa-a",
		"a-aa",
		"a-b-c",
		"valid",
		"valid-123",
	}
	for _, name := range valid {
		err := naming.ValidateSlot(name)
		c.Assert(err, IsNil)
		err = naming.ValidatePlug(name)
		c.Assert(err, IsNil)
		err = naming.ValidateInterface(name)
		c.Assert(err, IsNil)
	}
	invalid := []string{
		"",
		"a a",
		"a--a",
		"-a",
		"a-",
		"0",
		"123",
		"123abc",
		"日本語",
	}
	for _, name := range invalid {
		err := naming.ValidateSlot(name)
		c.Assert(err, ErrorMatches, `invalid slot name: ".*"`)
		err = naming.ValidatePlug(name)
		c.Assert(err, ErrorMatches, `invalid plug name: ".*"`)
		err = naming.ValidateInterface(name)
		c.Assert(err, ErrorMatches, `invalid interface name: ".*"`)
	}
}

func (s *ValidateSuite) TestValidateSnapID(c *C) {
	c.Check(naming.ValidateSnapID("buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ"), IsNil)

	invalid := []string{
		"",
		"buPKUD3TKqC",
		"buPKUD3TKqCOgLE-jHx5kSiCpIs5cMuQ",
		"buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQxxx",
	}
	for _, id := range invalid {
		err := naming.ValidateSnapID(id)
		c.Check(err, ErrorMatches, fmt.Sprintf("invalid snap-id: %q", id))
	}
}

func (s *ValidateSuite) TestValidateSecurityTag(c *C) {
	// valid snap names, snap instances, app names and hook names are accepted.
	c.Check(naming.ValidateSecurityTag("snap.pkg.app"), IsNil)
	c.Check(naming.ValidateSecurityTag("snap.pkg.hook.configure"), IsNil)
	c.Check(naming.ValidateSecurityTag("snap.pkg_key.app"), IsNil)
	c.Check(naming.ValidateSecurityTag("snap.pkg_key.hook.configure"), IsNil)

	// invalid format is rejected
	c.Check(naming.ValidateSecurityTag("snap.pkg_key.app.surprise"), ErrorMatches, "invalid security tag")
	c.Check(naming.ValidateSecurityTag("snap.pkg_key.hook.configure.surprise"), ErrorMatches, "invalid security tag")

	// invalid snap and app names are rejected.
	c.Check(naming.ValidateSecurityTag("snap._.app"), ErrorMatches, "invalid security tag")
	c.Check(naming.ValidateSecurityTag("snap.pkg._"), ErrorMatches, "invalid security tag")

	// invalid number of components are rejected.
	c.Check(naming.ValidateSecurityTag("snap.pkg.hook.surprise."), ErrorMatches, "invalid security tag")
	c.Check(naming.ValidateSecurityTag("snap.pkg.hook."), ErrorMatches, "invalid security tag")
	c.Check(naming.ValidateSecurityTag("snap.pkg.hook"), IsNil) // Perhaps somewhat unexpectedly, this tag is valid.
	c.Check(naming.ValidateSecurityTag("snap.pkg.app.surprise"), ErrorMatches, "invalid security tag")
	c.Check(naming.ValidateSecurityTag("snap.pkg."), ErrorMatches, "invalid security tag")
	c.Check(naming.ValidateSecurityTag("snap.pkg"), ErrorMatches, "invalid security tag")
	c.Check(naming.ValidateSecurityTag("snap."), ErrorMatches, "invalid security tag")
	c.Check(naming.ValidateSecurityTag("snap"), ErrorMatches, "invalid security tag")
}

func (s *ValidateSuite) TestValidQuotaGroup(c *C) {
	validNames := []string{
		"aa", "aaa", "aaaa",
		"a-a", "aa-a", "a-aa", "a-b-c",
		"a0", "a-0", "a-0a",
		"01game", "1-or-2",
		// a regexp stresser
		"u-94903713687486543234157734673284536758",
	}
	for _, name := range validNames {
		err := naming.ValidateQuotaGroup(name)
		c.Assert(err, IsNil)
	}
	invalidNames := []string{
		// name cannot be empty
		"",
		// too short (min 2 chars)
		"a",
		// names cannot be too long
		"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
		"xxxxxxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxx",
		"1111111111111111111111111111111111111111x",
		"x1111111111111111111111111111111111111111",
		"x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x",
		// a regexp stresser
		"u-9490371368748654323415773467328453675-",
		// dashes alone are not a name
		"-", "--",
		// double dashes in a name are not allowed
		"a--a",
		// name should not end with a dash
		"a-",
		// name cannot have any spaces in it
		"a ", " a", "a a",
		// a number alone is not a name
		"0", "123",
		// identifier must be plain ASCII
		"日本語", "한글", "ру́сский язы́к",
	}
	for _, name := range invalidNames {
		err := naming.ValidateQuotaGroup(name)
		c.Assert(err, ErrorMatches, `invalid quota group name:.*`)
	}
}

func (s *ValidateSuite) TestValidateProvenance(c *C) {
	c.Check(naming.ValidateProvenance("a"), IsNil)
	c.Check(naming.ValidateProvenance("123A-abz-dd3Z9"), IsNil)

	c.Check(naming.ValidateProvenance(""), ErrorMatches, `invalid provenance: must not be empty`)

	invalid := []string{
		"+",
		"-",
		"--",
		"a--z",
	}
	for _, prov := range invalid {
		err := naming.ValidateProvenance(prov)
		c.Check(err, ErrorMatches, regexp.QuoteMeta(fmt.Sprintf("invalid provenance: %q", prov)))
	}
}

func (s *ValidateSuite) TestValidateAssumes(c *C) {
	fullFeatureSet := map[string]bool{
		"common-data-dir":  true,
		"snap-env":         true,
		"command-chain":    true,
		"kernel-assets":    true,
		"app-refresh-mode": true,
		"snap-uid-envvars": true,
	}

	var assumesTests = []struct {
		assumes  []string
		version  string
		features map[string]bool
		err      string
	}{{
		assumes:  []string{"common-data-dir"},
		features: fullFeatureSet,
	}, {
		assumes:  []string{"f1", "f2"},
		features: fullFeatureSet,
		err:      `unsupported features: f1, f2`,
	}, {
		assumes:  []string{"snapd2.15"},
		features: fullFeatureSet,
		version:  "unknown",
	}, {
		assumes:  []string{"snapdnono"},
		features: fullFeatureSet,
		version:  "unknown",
		err:      `unsupported features: snapdnono`,
	}, {
		assumes:  []string{"snapd2.15nono"},
		version:  "unknown",
		features: fullFeatureSet,
		err:      `unsupported features: snapd2.15nono`,
	}, {
		assumes:  []string{"snapd2.15~pre1"},
		version:  "unknown",
		features: fullFeatureSet,
		err:      `unsupported features: snapd2.15~pre1`,
	}, {
		assumes:  []string{"snapd2.15"},
		version:  "2.15",
		features: fullFeatureSet,
	}, {
		assumes:  []string{"snapd2.15"},
		version:  "2.15.1",
		features: fullFeatureSet,
	}, {
		assumes:  []string{"snapd2.15"},
		version:  "2.15+git",
		features: fullFeatureSet,
	}, {
		assumes:  []string{"snapd2.15"},
		version:  "2.16",
		features: fullFeatureSet,
	}, {
		assumes:  []string{"snapd2.15.1"},
		version:  "2.16",
		features: fullFeatureSet,
	}, {
		assumes:  []string{"snapd2.15.1"},
		version:  "2.15.1",
		features: fullFeatureSet,
	}, {
		assumes:  []string{"snapd2.15.1.2"},
		version:  "2.15.1.2",
		features: fullFeatureSet,
	}, {
		assumes:  []string{"snapd2.15.1.2"},
		version:  "2.15.1.3",
		features: fullFeatureSet,
	}, {
		// the horror the horror!
		assumes:  []string{"snapd2.15.1.2.4.5.6.7.8.8"},
		version:  "2.15.1.2.4.5.6.7.8.8",
		features: fullFeatureSet,
	}, {
		assumes:  []string{"snapd2.15.1.2.4.5.6.7.8.8"},
		version:  "2.15.1.2.4.5.6.7.8.9",
		features: fullFeatureSet,
	}, {
		assumes:  []string{"snapd2.15.1.2"},
		version:  "2.15.1.3",
		features: fullFeatureSet,
	}, {
		assumes:  []string{"snapd2.15.2"},
		version:  "2.16.1",
		features: fullFeatureSet,
	}, {
		assumes:  []string{"snapd2.1000"},
		version:  "3.1",
		features: fullFeatureSet,
	}, {
		assumes:  []string{"snapd3"},
		version:  "3.1",
		features: fullFeatureSet,
	}, {
		assumes:  []string{"snapd2"},
		version:  "3.1",
		features: fullFeatureSet,
	}, {
		assumes:  []string{"snapd3"},
		version:  "2.48",
		features: fullFeatureSet,
		err:      `unsupported features: snapd3`,
	}, {
		assumes:  []string{"snapd2.15.1.2"},
		version:  "2.15.1.1",
		features: fullFeatureSet,
		err:      `unsupported features: snapd2\.15\.1\.2`,
	}, {
		assumes:  []string{"snapd2.15.1.2.4.5.6.7.8.8"},
		version:  "2.15.1.2.4.5.6.7.8.1",
		features: fullFeatureSet,
		err:      `unsupported features: snapd2\.15\.1\.2\.4\.5\.6\.7\.8\.8`,
	}, {
		assumes:  []string{"snapd2.16"},
		version:  "2.15",
		features: fullFeatureSet,
		err:      `unsupported features: snapd2\.16`,
	}, {
		assumes:  []string{"snapd2.15.1"},
		version:  "2.15",
		features: fullFeatureSet,
		err:      `unsupported features: snapd2\.15\.1`,
	}, {
		assumes:  []string{"snapd2.15.1"},
		version:  "2.15.0",
		features: fullFeatureSet,
		err:      `unsupported features: snapd2\.15\.1`,
	}, {
		// Note that this is different from how strconv.VersionCompare
		// (dpkg version numbering) would behave - it would error here
		assumes:  []string{"snapd2.15"},
		version:  "2.15~pre1",
		features: fullFeatureSet,
	}, {
		assumes:  []string{"command-chain"},
		features: fullFeatureSet,
	}, {
		assumes:  []string{"kernel-assets"},
		features: fullFeatureSet,
	}, {
		assumes:  []string{"snap-uid-envvars"},
		features: fullFeatureSet,
	}, {
		assumes:  []string{"valid-format"},
		version:  "",
		features: nil,
	}, {
		assumes:  []string{"UPPERCASE"},
		version:  "",
		features: nil,
		err:      `invalid features: UPPERCASE`,
	}, {
		assumes:  []string{"-starts-dash"},
		version:  "",
		features: nil,
		err:      `invalid features: -starts-dash`,
	}, {
		assumes:  []string{"double--dash"},
		version:  "",
		features: nil,
		err:      `invalid features: double--dash`,
	}, {
		assumes:  []string{"common-data-dir", "bad-feature"},
		version:  "",
		features: map[string]bool{"common-data-dir": true},
		err:      `unsupported features: bad-feature`,
	},
	}

	for _, test := range assumesTests {
		err := naming.ValidateAssumes(test.assumes, test.version, test.features)
		if test.err == "" {
			c.Check(err, IsNil)
		} else {
			c.Check(err, ErrorMatches, test.err)
		}
	}
}
