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

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

import (
	"encoding/json"
	"fmt"
	"net/http"
	"strings"

	"gopkg.in/check.v1"
	. "gopkg.in/check.v1"

	snapset "github.com/snapcore/snapd/cmd/snap"
	"github.com/snapcore/snapd/strutil"
)

type getCmdArgs struct {
	args, stdout, stderr, error string
	isTerminal                  bool
}

var getTests = []getCmdArgs{{
	args:  "get snap-name --foo",
	error: ".*unknown flag.*foo.*",
}, {
	args:   "get snapname test-key1",
	stdout: "test-value1\n",
}, {
	args:   "get snapname test-key2",
	stdout: "2\n",
}, {
	args:   "get snapname missing-key",
	stdout: "\n",
}, {
	args:   "get -t snapname test-key1",
	stdout: "\"test-value1\"\n",
}, {
	args:   "get -t snapname test-key2",
	stdout: "2\n",
}, {
	args:   "get -t snapname missing-key",
	stdout: "null\n",
}, {
	args:   "get -d snapname test-key1",
	stdout: "{\n\t\"test-key1\": \"test-value1\"\n}\n",
}, {
	args:   "get -l snapname test-key1",
	stdout: "Key        Value\ntest-key1  test-value1\n",
}, {
	args:   "get snapname -l test-key1 test-key2",
	stdout: "Key        Value\ntest-key1  test-value1\ntest-key2  2\n",
}, {
	args:   "get snapname document",
	stderr: `WARNING: The output of 'snap get' will become a list with columns - use -d or -l to force the output format.\n`,
	stdout: "{\n\t\"document\": {\n\t\t\"key1\": \"value1\",\n\t\t\"key2\": \"value2\"\n\t}\n}\n",
}, {
	isTerminal: true,
	args:       "get snapname document",
	stdout:     "Key            Value\ndocument.key1  value1\ndocument.key2  value2\n",
}, {
	args:   "get snapname -d test-key1 test-key2",
	stdout: "{\n\t\"test-key1\": \"test-value1\",\n\t\"test-key2\": 2\n}\n",
}, {
	args:   "get snapname -l document",
	stdout: "Key            Value\ndocument.key1  value1\ndocument.key2  value2\n",
}, {
	args:   "get -d snapname document",
	stdout: "{\n\t\"document\": {\n\t\t\"key1\": \"value1\",\n\t\t\"key2\": \"value2\"\n\t}\n}\n",
}, {
	args:   "get -l snapname",
	stdout: "Key  Value\nbar  100\nfoo  {...}\n",
}, {
	args:   "get snapname -l test-key3 test-key4",
	stdout: "Key          Value\ntest-key3.a  1\ntest-key3.b  2\ntest-key3-a  9\ntest-key4.a  3\ntest-key4.b  4\n",
}, {
	args:   "get -d snapname",
	stdout: "{\n\t\"bar\": 100,\n\t\"foo\": {\n\t\t\"key1\": \"value1\",\n\t\t\"key2\": \"value2\"\n\t}\n}\n",
}, {
	isTerminal: true,
	args:       "get snapname  test-key1 test-key2",
	stdout:     "Key        Value\ntest-key1  test-value1\ntest-key2  2\n",
}, {
	isTerminal: false,
	args:       "get snapname  test-key1 test-key2",
	stdout:     "{\n\t\"test-key1\": \"test-value1\",\n\t\"test-key2\": 2\n}\n",
	stderr:     `WARNING: The output of 'snap get' will become a list with columns - use -d or -l to force the output format.\n`,
},
}

func (s *SnapSuite) runTests(cmds []getCmdArgs, c *C) {
	for _, test := range cmds {
		s.stdout.Truncate(0)
		s.stderr.Truncate(0)

		c.Logf("Test: %s", test.args)

		restore := snapset.MockIsStdoutTTY(test.isTerminal)
		defer restore()

		_, err := snapset.Parser(snapset.Client()).ParseArgs(strings.Fields(test.args))
		if test.error != "" {
			c.Check(err, ErrorMatches, test.error)
		} else {
			c.Check(err, IsNil)
			c.Check(s.Stderr(), Equals, test.stderr)
			c.Check(s.Stdout(), Equals, test.stdout)
		}
	}
}

func (s *SnapSuite) TestSnapGetTests(c *C) {
	s.mockGetConfigServer(c)
	s.runTests(getTests, c)
}

var getNoConfigTests = []getCmdArgs{{
	args:  "get -l snapname",
	error: `snap "snapname" has no configuration`,
}, {
	args:  "get snapname",
	error: `snap "snapname" has no configuration`,
}, {
	args:   "get -d snapname",
	stdout: "{}\n",
}}

func (s *SnapSuite) TestSnapGetNoConfiguration(c *C) {
	s.mockGetEmptyConfigServer(c)
	s.runTests(getNoConfigTests, c)
}

func (s *SnapSuite) TestSortByPath(c *C) {
	values := []snapset.ConfigValue{
		{Path: "test-key3.b"},
		{Path: "a"},
		{Path: "test-key3.a"},
		{Path: "a.b.c"},
		{Path: "test-key4.a"},
		{Path: "test-key4.b"},
		{Path: "a-b"},
		{Path: "zzz"},
		{Path: "aa"},
		{Path: "test-key3-a"},
		{Path: "a.b"},
	}
	snapset.SortByPath(values)

	expected := []string{
		"a",
		"a.b",
		"a.b.c",
		"a-b",
		"aa",
		"test-key3.a",
		"test-key3.b",
		"test-key3-a",
		"test-key4.a",
		"test-key4.b",
		"zzz",
	}

	c.Assert(values, HasLen, len(expected))

	for i, e := range expected {
		c.Assert(values[i].Path, Equals, e)
	}
}

func (s *SnapSuite) mockGetConfigServer(c *C) {
	s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/v2/snaps/snapname/conf" {
			c.Errorf("unexpected path %q", r.URL.Path)
			return
		}

		c.Check(r.Method, Equals, "GET")

		query := r.URL.Query()
		switch query.Get("keys") {
		case "test-key1":
			fmt.Fprintln(w, `{"type":"sync", "status-code": 200, "result": {"test-key1":"test-value1"}}`)
		case "test-key2":
			fmt.Fprintln(w, `{"type":"sync", "status-code": 200, "result": {"test-key2":2}}`)
		case "test-key1,test-key2":
			fmt.Fprintln(w, `{"type":"sync", "status-code": 200, "result": {"test-key1":"test-value1","test-key2":2}}`)
		case "test-key3,test-key4":
			fmt.Fprintln(w, `{"type":"sync", "status-code": 200, "result": {"test-key3":{"a":1,"b":2},"test-key3-a":9,"test-key4":{"a":3,"b":4}}}`)
		case "missing-key":
			fmt.Fprintln(w, `{"type":"sync", "status-code": 200, "result": {}}`)
		case "document":
			fmt.Fprintln(w, `{"type":"sync", "status-code": 200, "result": {"document":{"key1":"value1","key2":"value2"}}}`)
		case "":
			fmt.Fprintln(w, `{"type":"sync", "status-code": 200, "result": {"foo":{"key1":"value1","key2":"value2"},"bar":100}}`)
		default:
			c.Errorf("unexpected keys %q", query.Get("keys"))
		}
	})
}

func (s *SnapSuite) mockGetEmptyConfigServer(c *C) {
	s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/v2/snaps/snapname/conf" {
			c.Errorf("unexpected path %q", r.URL.Path)
			return
		}

		c.Check(r.Method, Equals, "GET")

		fmt.Fprintln(w, `{"type":"sync", "status-code": 200, "result": {}}`)
	})
}

func (s *confdbSuite) TestConfdbGet(c *C) {
	restore := snapset.MockIsStdinTTY(true)
	defer restore()

	restore = s.mockConfdbFlag(c)
	defer restore()

	var reqs int
	s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
		switch reqs {
		case 0:
			c.Check(r.Method, Equals, "GET")
			c.Check(r.URL.Path, Equals, "/v2/confdb/foo/bar/baz")

			q := r.URL.Query()
			keys := strutil.CommaSeparatedList(q.Get("keys"))
			c.Check(keys, DeepEquals, []string{"abc"})

			w.WriteHeader(202)
			fmt.Fprintf(w, asyncResp)
		case 1:
			c.Check(r.Method, check.Equals, "GET")
			c.Check(r.URL.Path, check.Equals, "/v2/changes/123")
			fmt.Fprintln(w, `{"type": "sync", "result": {"status": "Doing"}}`)
		case 2:
			c.Check(r.Method, check.Equals, "GET")
			c.Check(r.URL.Path, check.Equals, "/v2/changes/123")
			fmt.Fprintln(w, `{"type": "sync", "result": {"ready": true, "status": "Done", "data": {"values": {"abc": "cba"}}}}`)
		default:
			err := fmt.Errorf("expected to get 1 request, now on %d (%v)", reqs+1, r)
			w.WriteHeader(500)
			fmt.Fprintf(w, `{"type": "error", "result": {"message": %q}}`, err)
			c.Error(err)
		}

		reqs++
	})

	rest, err := snapset.Parser(snapset.Client()).ParseArgs([]string{"get", "foo/bar/baz", "abc"})
	c.Assert(err, IsNil)
	c.Assert(rest, HasLen, 0)
	c.Check(s.Stdout(), Equals, "cba\n")
	c.Check(s.Stderr(), Equals, "")
}

func (s *confdbSuite) TestConfdbGetAsDocument(c *C) {
	restore := snapset.MockIsStdinTTY(true)
	defer restore()

	restore = s.mockConfdbFlag(c)
	defer restore()

	var reqs int
	s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
		switch reqs {
		case 0:
			c.Check(r.Method, Equals, "GET")
			c.Check(r.URL.Path, Equals, "/v2/confdb/foo/bar/baz")

			q := r.URL.Query()
			keys := strutil.CommaSeparatedList(q.Get("keys"))
			c.Check(keys, DeepEquals, []string{"abc"})

			w.WriteHeader(202)
			fmt.Fprintf(w, asyncResp)
		case 1:
			c.Check(r.Method, check.Equals, "GET")
			c.Check(r.URL.Path, check.Equals, "/v2/changes/123")
			fmt.Fprintln(w, `{"type": "sync", "result": {"status": "Doing"}}`)
		case 2:
			c.Check(r.Method, check.Equals, "GET")
			c.Check(r.URL.Path, check.Equals, "/v2/changes/123")
			fmt.Fprintln(w, `{"type": "sync", "result": {"ready": true, "status": "Done", "data": {"values": {"abc": "cba"}}}}`)
		default:
			err := fmt.Errorf("expected to get 1 request, now on %d (%v)", reqs+1, r)
			w.WriteHeader(500)
			fmt.Fprintf(w, `{"type": "error", "result": {"message": %q}}`, err)
			c.Error(err)
		}

		reqs++
	})

	rest, err := snapset.Parser(snapset.Client()).ParseArgs([]string{"get", "-d", "foo/bar/baz", "abc"})
	c.Assert(err, IsNil)
	c.Assert(rest, HasLen, 0)

	c.Check(s.Stdout(), Equals, `{
	"abc": "cba"
}
`)
	c.Check(s.Stderr(), Equals, "")
}

func (s *confdbSuite) TestConfdbGetMany(c *C) {
	restore := snapset.MockIsStdoutTTY(true)
	defer restore()

	restore = s.mockConfdbFlag(c)
	defer restore()

	var reqs int
	s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
		switch reqs {
		case 0:
			c.Check(r.Method, Equals, "GET")
			c.Check(r.URL.Path, Equals, "/v2/confdb/foo/bar/baz")

			q := r.URL.Query()
			keys := strutil.CommaSeparatedList(q.Get("keys"))
			c.Check(keys, DeepEquals, []string{"abc", "xyz"})

			w.WriteHeader(202)
			fmt.Fprintf(w, asyncResp)
		case 1:
			c.Check(r.Method, check.Equals, "GET")
			c.Check(r.URL.Path, check.Equals, "/v2/changes/123")
			fmt.Fprintln(w, `{"type": "sync", "result": {"status": "Doing"}}`)
		case 2:
			c.Check(r.Method, check.Equals, "GET")
			c.Check(r.URL.Path, check.Equals, "/v2/changes/123")
			fmt.Fprintln(w, `{"type": "sync", "result": {"ready": true, "status": "Done", "data": {"values": {"abc": 1, "xyz": false}}}}`)
		default:
			err := fmt.Errorf("expected to get 1 request, now on %d (%v)", reqs+1, r)
			w.WriteHeader(500)
			fmt.Fprintf(w, `{"type": "error", "result": {"message": %q}}`, err)
			c.Error(err)
		}

		reqs++
	})

	rest, err := snapset.Parser(snapset.Client()).ParseArgs([]string{"get", "foo/bar/baz", "abc", "xyz"})
	c.Assert(err, IsNil)
	c.Assert(rest, HasLen, 0)
	c.Check(s.Stdout(), Equals,
		`Key  Value
abc  1
xyz  false
`)
	c.Check(s.Stderr(), Equals, "")
}

func (s *confdbSuite) TestConfdbGetManyAsDocument(c *C) {
	restore := snapset.MockIsStdoutTTY(true)
	defer restore()

	restore = s.mockConfdbFlag(c)
	defer restore()

	var reqs int
	s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
		switch reqs {
		case 0:
			c.Check(r.Method, Equals, "GET")
			c.Check(r.URL.Path, Equals, "/v2/confdb/foo/bar/baz")

			q := r.URL.Query()
			keys := strutil.CommaSeparatedList(q.Get("keys"))
			c.Check(keys, DeepEquals, []string{"abc", "xyz"})

			w.WriteHeader(202)
			fmt.Fprintf(w, asyncResp)
		case 1:
			c.Check(r.Method, check.Equals, "GET")
			c.Check(r.URL.Path, check.Equals, "/v2/changes/123")
			fmt.Fprintln(w, `{"type": "sync", "result": {"status": "Doing"}}`)
		case 2:
			c.Check(r.Method, check.Equals, "GET")
			c.Check(r.URL.Path, check.Equals, "/v2/changes/123")
			fmt.Fprintln(w, `{"type": "sync", "result": {"ready": true, "status": "Done", "data": {"values": {"abc": 1, "xyz": false}}}}`)
		default:
			err := fmt.Errorf("expected to get 1 request, now on %d (%v)", reqs+1, r)
			w.WriteHeader(500)
			fmt.Fprintf(w, `{"type": "error", "result": {"message": %q}}`, err)
			c.Error(err)
		}

		reqs++
	})

	rest, err := snapset.Parser(snapset.Client()).ParseArgs([]string{"get", "-d", "foo/bar/baz", "abc", "xyz"})
	c.Assert(err, IsNil)
	c.Assert(rest, HasLen, 0)

	c.Check(s.Stdout(), Equals, `{
	"abc": 1,
	"xyz": false
}
`)
	c.Check(s.Stderr(), Equals, "")
}

func (s *confdbSuite) TestConfdbGetInvalidConfdbSchemaID(c *check.C) {
	restore := s.mockConfdbFlag(c)
	defer restore()

	_, err := snapset.Parser(snapset.Client()).ParseArgs([]string{"get", "foo//bar", "foo=bar"})
	c.Assert(err, NotNil)
	c.Check(err.Error(), Equals, "confdb-schema view id must conform to format: <account-id>/<confdb-schema>/<view>")
}

func (s *confdbSuite) TestConfdbGetDisabledFlag(c *check.C) {
	var reqs int
	s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
		switch reqs {
		default:
			err := fmt.Errorf("expected to get no requests, now on %d (%v)", reqs+1, r)
			w.WriteHeader(500)
			fmt.Fprintf(w, `{"type": "error", "result": {"message": %q}}`, err)
			c.Error(err)
		}

		reqs++
	})

	_, err := snapset.Parser(snapset.Client()).ParseArgs([]string{"get", "foo/bar/baz", "abc"})
	c.Assert(err, check.ErrorMatches, `the "confdb" feature is disabled: set 'experimental.confdb' to true`)
}

func (s *confdbSuite) TestConfdbGetNoFields(c *check.C) {
	restore := s.mockConfdbFlag(c)
	defer restore()

	var reqs int
	s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
		switch reqs {
		case 0:
			c.Check(r.Method, Equals, "GET")
			c.Check(r.URL.Path, Equals, "/v2/confdb/foo/bar/baz")

			q := r.URL.Query()
			keys := strutil.CommaSeparatedList(q.Get("keys"))
			c.Check(keys, HasLen, 0)

			w.WriteHeader(202)
			fmt.Fprintf(w, asyncResp)
		case 1:
			c.Check(r.Method, check.Equals, "GET")
			c.Check(r.URL.Path, check.Equals, "/v2/changes/123")
			fmt.Fprintln(w, `{"type": "sync", "result": {"status": "Doing"}}`)
		case 2:
			c.Check(r.Method, check.Equals, "GET")
			c.Check(r.URL.Path, check.Equals, "/v2/changes/123")
			fmt.Fprintln(w, `{"type": "sync", "result": {"ready": true, "status": "Done", "data": {"values": {"abc": 1, "xyz": false}}}}`)
		default:
			err := fmt.Errorf("expected to get 1 request, now on %d (%v)", reqs+1, r)
			w.WriteHeader(500)
			fmt.Fprintf(w, `{"type": "error", "result": {"message": %q}}`, err)
			c.Error(err)
		}

		reqs++
	})

	rest, err := snapset.Parser(snapset.Client()).ParseArgs([]string{"get", "foo/bar/baz"})
	c.Assert(err, IsNil)
	c.Assert(rest, HasLen, 0)

	c.Check(s.Stdout(), Equals, `{
	"abc": 1,
	"xyz": false
}
`)
}

func (s *confdbSuite) TestConfdbGetNoWait(c *check.C) {
	restore := s.mockConfdbFlag(c)
	defer restore()

	_, err := snapset.Parser(snapset.Client()).ParseArgs([]string{"get", "foo/bar/baz", "--no-wait"})
	// although a confdb snap get waits for a change, there's no --no-wait option
	c.Assert(err, ErrorMatches, "unknown flag `no-wait'")
}

func (s *confdbSuite) TestConfdbGetNoDataError(c *check.C) {
	restore := s.mockConfdbFlag(c)
	defer restore()

	var reqs int
	s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
		switch reqs {
		case 0:
			c.Check(r.Method, Equals, "GET")
			c.Check(r.URL.Path, Equals, "/v2/confdb/foo/bar/baz")

			q := r.URL.Query()
			keys := strutil.CommaSeparatedList(q.Get("keys"))
			c.Check(keys, DeepEquals, []string{"foo"})

			w.WriteHeader(202)
			fmt.Fprintf(w, asyncResp)
		case 1:
			c.Check(r.Method, check.Equals, "GET")
			c.Check(r.URL.Path, check.Equals, "/v2/changes/123")
			fmt.Fprintln(w, `{"type": "sync", "result": {"ready": true, "status": "Done", "data": {"error": {"message": "some error, no data", "kind": "option-not-found"}}}}`)
		default:
			err := fmt.Errorf("expected to get 1 request, now on %d (%v)", reqs+1, r)
			w.WriteHeader(500)
			fmt.Fprintf(w, `{"type": "error", "result": {"message": %q}}`, err)
			c.Error(err)
		}

		reqs++
	})

	_, err := snapset.Parser(snapset.Client()).ParseArgs([]string{"get", "foo/bar/baz", "foo"})
	c.Assert(err, ErrorMatches, "some error, no data")
}

func (s *confdbSuite) TestConfdbGetInternalError(c *check.C) {
	restore := s.mockConfdbFlag(c)
	defer restore()

	var reqs int
	s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
		switch reqs {
		case 0:
			c.Check(r.Method, Equals, "GET")
			c.Check(r.URL.Path, Equals, "/v2/confdb/foo/bar/baz")

			q := r.URL.Query()
			keys := strutil.CommaSeparatedList(q.Get("keys"))
			c.Check(keys, DeepEquals, []string{"foo"})

			w.WriteHeader(202)
			fmt.Fprintf(w, asyncResp)
		case 1:
			c.Check(r.Method, check.Equals, "GET")
			c.Check(r.URL.Path, check.Equals, "/v2/changes/123")
			w.WriteHeader(500)
			fmt.Fprintln(w, `{"type": "error", "result": {"message": "internal error: something went wrong"}}`)
		default:
			err := fmt.Errorf("expected to get 1 request, now on %d (%v)", reqs+1, r)
			w.WriteHeader(500)
			fmt.Fprintf(w, `{"type": "error", "result": {"message": %q}}`, err)
			c.Error(err)
		}

		reqs++
	})

	_, err := snapset.Parser(snapset.Client()).ParseArgs([]string{"get", "foo/bar/baz", "foo"})
	c.Assert(err, ErrorMatches, "internal error: something went wrong")
}

func (s *confdbSuite) TestConfdbDefaultMultipleTypes(c *check.C) {
	restore := s.mockConfdbFlag(c)
	defer restore()

	var reqs int
	s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
		switch reqs {
		case 0, 2, 4, 6, 8:
			c.Check(r.Method, Equals, "GET")
			c.Check(r.URL.Path, Equals, "/v2/confdb/foo/bar/baz")

			q := r.URL.Query()
			keys := strutil.CommaSeparatedList(q.Get("keys"))
			c.Check(keys, DeepEquals, []string{"foo"})

			w.WriteHeader(202)
			fmt.Fprintf(w, asyncResp)
		case 1, 3, 5, 7, 9:
			c.Check(r.Method, check.Equals, "GET")
			c.Check(r.URL.Path, check.Equals, "/v2/changes/123")
			fmt.Fprintln(w, `{"type": "sync", "result": {"ready": true, "status": "Done", "data": {"error": {"message": "some error, no data", "kind": "option-not-found"}}}}`)
		default:
			err := fmt.Errorf("expected to get 1 request, now on %d (%v)", reqs+1, r)
			w.WriteHeader(500)
			fmt.Fprintf(w, `{"type": "error", "result": {"message": %q}}`, err)
			c.Error(err)
		}

		reqs++
	})

	type testcase struct {
		input  any
		output string
	}

	tcs := []testcase{
		{
			input:  "\"bar\"",
			output: "bar",
		},
		{
			input:  1,
			output: "1",
		},
		{
			input: `[1, 2]`,
			output: `[
	1,
	2
]`,
		},
		{
			input:  "unquoted",
			output: "unquoted",
		},
	}

	for _, tc := range tcs {
		s.stdout.Reset()
		rest, err := snapset.Parser(snapset.Client()).ParseArgs([]string{"get", "foo/bar/baz", fmt.Sprintf("--default=%v", tc.input), "foo"})
		c.Assert(err, IsNil)
		c.Assert(rest, HasLen, 0)
		c.Check(s.Stdout(), Equals, fmt.Sprintf("%v\n", tc.output))
	}
}

func (s *confdbSuite) TestConfdbGetDefaultWithOtherFlags(c *check.C) {
	restore := snapset.MockIsStdoutTTY(true)
	defer restore()

	restore = s.mockConfdbFlag(c)
	defer restore()

	var reqs int
	s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
		switch reqs {
		case 0, 2, 4, 6, 8, 10, 12:
			c.Check(r.Method, Equals, "GET")
			c.Check(r.URL.Path, Equals, "/v2/confdb/foo/bar/baz")

			q := r.URL.Query()
			keys := strutil.CommaSeparatedList(q.Get("keys"))
			c.Check(keys, DeepEquals, []string{"foo[0].bar"})

			w.WriteHeader(202)
			fmt.Fprintf(w, asyncResp)

		case 1, 3, 5, 7, 9, 11, 13:
			c.Check(r.Method, check.Equals, "GET")
			c.Check(r.URL.Path, check.Equals, "/v2/changes/123")
			fmt.Fprintln(w, `{"type": "sync", "result": {"ready": true, "status": "Done", "data": {"error": {"message": "some error, no data", "kind": "option-not-found"}}}}`)

		default:
			err := fmt.Errorf("expected to get 1 request, now on %d (%v)", reqs+1, r)
			w.WriteHeader(500)
			fmt.Fprintf(w, `{"type": "error", "result": {"message": %q}}`, err)
			c.Error(err)
		}

		reqs++
	})

	type testcase struct {
		flags  []string
		output string
	}

	tcs := []testcase{
		{
			// if default isn't JSON, we fallback to treating it as a string (unless -t is on)
			flags:  []string{"--default", "baz"},
			output: "baz\n",
		},
		{
			flags: []string{"-d", "--default", "baz"},
			output: `{
	"foo[0].bar": "baz"
}
`,
		},
		{
			flags: []string{"-l", "--default", "baz"},
			output: `Key         Value
foo[0].bar  baz
`,
		},
		{
			flags:  []string{"-t", "--default", `"1"`},
			output: "\"1\"\n",
		},
		{
			flags:  []string{"-t", "--default", `"foo"`},
			output: "\"foo\"\n",
		},
		{
			flags: []string{"-t", "--default", `{"baz":1}`},
			output: `Key             Value
foo[0].bar.baz  1
`,
		},
		{
			flags:  []string{"-t", "--default", "123"},
			output: "123\n",
		},
	}

	for _, tc := range tcs {
		args := append([]string{"get", "foo/bar/baz", "foo[0].bar"}, tc.flags...)
		rest, err := snapset.Parser(snapset.Client()).ParseArgs(args)
		c.Assert(err, IsNil)
		c.Assert(rest, HasLen, 0)
		c.Check(s.Stdout(), Equals, tc.output)
		c.Check(s.Stderr(), Equals, "")
		s.stdout.Reset()
	}
}

func (s *confdbSuite) TestConfdbGetForbidDefaultWithMultiKeys(c *check.C) {
	restore := s.mockConfdbFlag(c)
	defer restore()

	var reqs int
	s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
		switch reqs {
		default:
			err := fmt.Errorf("expected to get no requests, now on %d (%v)", reqs+1, r)
			w.WriteHeader(500)
			fmt.Fprintf(w, `{"type": "error", "result": {"message": %q}}`, err)
			c.Error(err)
		}

		reqs++
	})

	_, err := snapset.Parser(snapset.Client()).ParseArgs([]string{"get", "foo/bar/baz", "--default", "defVal", "foo", "bar"})
	c.Assert(err, ErrorMatches, "cannot use --default with more than one confdb request")
	c.Check(s.Stdout(), Equals, "")
	c.Check(s.Stderr(), Equals, "")
}

func (s *confdbSuite) TestConfdbGetDefaultUnmarshalNoFallbackIfTyped(c *check.C) {
	restore := s.mockConfdbFlag(c)
	defer restore()

	var reqs int
	s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
		switch reqs {
		case 0:
			c.Check(r.Method, Equals, "GET")
			c.Check(r.URL.Path, Equals, "/v2/confdb/foo/bar/baz")

			q := r.URL.Query()
			keys := strutil.CommaSeparatedList(q.Get("keys"))
			c.Check(keys, DeepEquals, []string{"foo"})

			w.WriteHeader(202)
			fmt.Fprintf(w, asyncResp)

		case 1:
			c.Check(r.Method, check.Equals, "GET")
			c.Check(r.URL.Path, check.Equals, "/v2/changes/123")
			fmt.Fprintln(w, `{"type": "sync", "result": {"ready": true, "status": "Done", "data": {"error": {"message": "some error, no data", "kind": "option-not-found"}}}}`)

		default:
			err := fmt.Errorf("expected to get no requests, now on %d (%v)", reqs+1, r)
			w.WriteHeader(500)
			fmt.Fprintf(w, `{"type": "error", "result": {"message": %q}}`, err)
			c.Error(err)
		}

		reqs++
	})

	_, err := snapset.Parser(snapset.Client()).ParseArgs([]string{"get", "foo/bar/baz", "--default", "defVal", "-t", "foo"})
	c.Assert(err, ErrorMatches, "cannot unmarshal default value as strictly typed")
	c.Check(s.Stdout(), Equals, "")
	c.Check(s.Stderr(), Equals, "")
}

func (s *confdbSuite) TestForbidDefaultWithNonConfdbGet(c *check.C) {
	restore := s.mockConfdbFlag(c)
	defer restore()

	var reqs int
	s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
		switch reqs {
		default:
			err := fmt.Errorf("expected to get no requests, now on %d (%v)", reqs+1, r)
			w.WriteHeader(500)
			fmt.Fprintf(w, `{"type": "error", "result": {"message": %q}}`, err)
			c.Error(err)
		}

		reqs++
	})

	_, err := snapset.Parser(snapset.Client()).ParseArgs([]string{"get", ":foo", "--default", "def", "bar"})
	c.Assert(err, ErrorMatches, "cannot use --default in non-confdb read")
	c.Check(s.Stdout(), Equals, "")
	c.Check(s.Stderr(), Equals, "")
}

func (s *confdbSuite) TestConfdbGetWithConstraints(c *check.C) {
	restore := snapset.MockIsStdinTTY(true)
	defer restore()

	restore = s.mockConfdbFlag(c)
	defer restore()

	var reqs int
	s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
		switch reqs {
		case 0:
			c.Check(r.Method, Equals, "GET")
			c.Check(r.URL.Path, Equals, "/v2/confdb/foo/bar/baz")

			q := r.URL.Query()
			keys := strutil.CommaSeparatedList(q.Get("keys"))
			c.Check(keys, DeepEquals, []string{"abc"})

			constraintsStr := q.Get("constraints")
			var gotConstraints map[string]string
			err := json.Unmarshal([]byte(constraintsStr), &gotConstraints)
			c.Assert(err, IsNil)
			c.Check(gotConstraints, DeepEquals, map[string]string{"field1": "value1", "field2": "value2"})

			w.WriteHeader(202)
			fmt.Fprintf(w, asyncResp)
		case 1:
			c.Check(r.Method, check.Equals, "GET")
			c.Check(r.URL.Path, check.Equals, "/v2/changes/123")
			fmt.Fprintln(w, `{"type": "sync", "result": {"status": "Doing"}}`)
		case 2:
			c.Check(r.Method, check.Equals, "GET")
			c.Check(r.URL.Path, check.Equals, "/v2/changes/123")
			fmt.Fprintln(w, `{"type": "sync", "result": {"ready": true, "status": "Done", "data": {"values": {"abc": "cba"}}}}`)
		default:
			err := fmt.Errorf("expected to get 3 requests, now on %d (%v)", reqs+1, r)
			w.WriteHeader(500)
			fmt.Fprintf(w, `{"type": "error", "result": {"message": %q}}`, err)
			c.Error(err)
		}

		reqs++
	})

	rest, err := snapset.Parser(snapset.Client()).ParseArgs([]string{"get", "foo/bar/baz", "abc", "--with", "field1=value1", "--with", "field2=value2"})
	c.Assert(err, IsNil)
	c.Assert(rest, HasLen, 0)
	c.Check(s.Stdout(), Equals, "cba\n")
	c.Check(s.Stderr(), Equals, "")
}

func (s *confdbSuite) TestConfdbGetTypedConstraints(c *check.C) {
	restore := snapset.MockIsStdinTTY(true)
	defer restore()

	restore = s.mockConfdbFlag(c)
	defer restore()

	type testcase struct {
		constraint string
		expected   string
	}

	tcs := []testcase{
		{
			constraint: `param="foo"`,
			expected:   `{"param":"foo"}`,
		},
		{
			constraint: `param=1.2`,
			expected:   `{"param":1.2}`,
		},
		{
			constraint: `param=2.0`,
			expected:   `{"param":2}`,
		},
		{
			constraint: `param=true`,
			expected:   `{"param":true}`,
		},
		{
			// should fallback to interpreting as a string
			constraint: `param=bar`,
			expected:   `{"param":"bar"}`,
		},
		{
			constraint: `param=null`,
			expected:   `{"param":"null"}`,
		},
		{
			constraint: `param=[1,2]`,
			expected:   `{"param":"[1,2]"}`,
		},
		{
			constraint: `param={"a":"b"}`,
			expected:   `{"param":"{\"a\":\"b\"}"}`,
		},
	}

	for i, tc := range tcs {
		cmt := Commentf("testcase %d/%d", i+1, len(tcs))
		var reqs int
		s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
			switch reqs {
			case 0:
				c.Check(r.Method, Equals, "GET", cmt)
				c.Check(r.URL.Path, Equals, "/v2/confdb/foo/bar/baz", cmt)

				q := r.URL.Query()
				keys := strutil.CommaSeparatedList(q.Get("keys"))
				c.Check(keys, DeepEquals, []string{"abc"}, cmt)

				cstrs := q.Get("constraints")
				c.Check(cstrs, Equals, tc.expected, cmt)

				w.WriteHeader(202)
				fmt.Fprintf(w, asyncResp)
				fmt.Fprintln(w, `{"type": "sync", "result": {"status": "Doing"}}`)
			case 1:
				c.Check(r.Method, check.Equals, "GET", cmt)
				c.Check(r.URL.Path, check.Equals, "/v2/changes/123", cmt)
				fmt.Fprintln(w, `{"type": "sync", "result": {"ready": true, "status": "Done", "data": {"values": {"abc": "cba"}}}}`)
			default:
				err := fmt.Errorf("expected to get 3 requests, now on %d (%v)", reqs+1, r)
				w.WriteHeader(500)
				fmt.Fprintf(w, `{"type": "error", "result": {"message": %q}}`, err)
				c.Error(err, cmt)
			}

			reqs++
		})

		_, err := snapset.Parser(snapset.Client()).ParseArgs([]string{"get", "foo/bar/baz", "abc", "--with", tc.constraint})
		c.Assert(err, IsNil, cmt)
		c.Check(s.Stdout(), Equals, "cba\n", cmt)
		c.Check(s.Stderr(), Equals, "", cmt)

		s.ResetStdStreams()
	}
}

func (s *confdbSuite) TestConfdbGetWithInvalidConstraint(c *check.C) {
	restore := s.mockConfdbFlag(c)
	defer restore()

	var reqs int
	s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
		switch reqs {
		default:
			err := fmt.Errorf("expected to get no requests, now on %d (%v)", reqs+1, r)
			w.WriteHeader(500)
			fmt.Fprintf(w, `{"type": "error", "result": {"message": %q}}`, err)
			c.Error(err)
		}

		reqs++
	})

	for _, tc := range []string{"invalid", "invalid=", "=invalid", "="} {
		_, err := snapset.Parser(snapset.Client()).ParseArgs([]string{"get", "foo/bar/baz", "abc", "--with", tc})
		c.Assert(err, ErrorMatches, fmt.Sprintf(`--with constraints must be in the form <param>=<constraint> but got %q instead`, tc))
		c.Check(s.Stdout(), Equals, "")
		c.Check(s.Stderr(), Equals, "")
	}
}

func (s *confdbSuite) TestConfdbGetTypedConstraintsNoFallback(c *check.C) {
	restore := s.mockConfdbFlag(c)
	defer restore()

	var reqs int
	s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
		switch reqs {
		default:
			err := fmt.Errorf("expected to get no requests, now on %d (%v)", reqs+1, r)
			w.WriteHeader(500)
			fmt.Fprintf(w, `{"type": "error", "result": {"message": %q}}`, err)
			c.Error(err)
		}

		reqs++
	})

	type testcase struct {
		constraint string
		err        string
	}

	tcs := []testcase{
		{
			constraint: "invalid",
			err:        `--with constraints must be in the form <param>=<constraint> but got "invalid" instead`,
		},
		{
			constraint: "invalid=",
			err:        `--with constraints must be in the form <param>=<constraint> but got "invalid=" instead`,
		},
		{
			constraint: "=invalid",
			err:        `--with constraints must be in the form <param>=<constraint> but got "=invalid" instead`,
		},
		{
			constraint: "=",
			err:        `--with constraints must be in the form <param>=<constraint> but got "=" instead`,
		},
		{
			constraint: "foo=bar",
			err:        `cannot unmarshal constraint as JSON as required by -t flag: bar`,
		},
		{
			constraint: "foo=[1,2,3]",
			err:        `--with constraints cannot take non-scalar JSON constraint: \[1,2,3\]`,
		},
		{
			constraint: `foo={"a":"b"}`,
			err:        `--with constraints cannot take non-scalar JSON constraint: {"a":"b"}`,
		},
		{
			constraint: "foo=null",
			err:        `--with constraints cannot take non-scalar JSON constraint: null`,
		},
	}

	for _, tc := range tcs {
		_, err := snapset.Parser(snapset.Client()).ParseArgs([]string{"get", "foo/bar/baz", "-t", "--with", tc.constraint})
		c.Assert(err, ErrorMatches, tc.err)
		c.Check(s.Stdout(), Equals, "")
		c.Check(s.Stderr(), Equals, "")
	}
}

func (s *confdbSuite) TestForbidWithWithNonConfdbGet(c *check.C) {
	restore := s.mockConfdbFlag(c)
	defer restore()

	var reqs int
	s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
		switch reqs {
		default:
			err := fmt.Errorf("expected to get no requests, now on %d (%v)", reqs+1, r)
			w.WriteHeader(500)
			fmt.Fprintf(w, `{"type": "error", "result": {"message": %q}}`, err)
			c.Error(err)
		}

		reqs++
	})

	_, err := snapset.Parser(snapset.Client()).ParseArgs([]string{"get", ":foo", "--with", "field=value", "bar"})
	c.Assert(err, ErrorMatches, "cannot use --with in non-confdb read")
	c.Check(s.Stdout(), Equals, "")
	c.Check(s.Stderr(), Equals, "")
}

func (s *confdbSuite) TestConfdbGetWaitFor(c *check.C) {
	restore := snapset.MockIsStdinTTY(true)
	defer restore()

	restore = s.mockConfdbFlag(c)
	defer restore()

	var reqs int
	s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
		switch reqs {
		case 0:
			c.Check(r.Method, Equals, "GET")
			c.Check(r.URL.Path, Equals, "/v2/confdb/foo/bar/baz")

			q := r.URL.Query()
			c.Check(q.Get("access-timeout"), Equals, "5s")

			w.WriteHeader(202)
			fmt.Fprintf(w, asyncResp)
		case 1:
			c.Check(r.Method, check.Equals, "GET")
			c.Check(r.URL.Path, check.Equals, "/v2/changes/123")
			fmt.Fprintln(w, `{"type": "sync", "result": {"ready": true, "status": "Done", "data": {"values": {"abc": "cba"}}}}`)
		default:
			err := fmt.Errorf("expected to get 2 requests, now on %d (%v)", reqs+1, r)
			w.WriteHeader(500)
			fmt.Fprintf(w, `{"type": "error", "result": {"message": %q}}`, err)
			c.Error(err)
		}

		reqs++
	})

	rest, err := snapset.Parser(snapset.Client()).ParseArgs([]string{"get", "foo/bar/baz", "abc", "--wait-for", "5s"})
	c.Assert(err, IsNil)
	c.Assert(rest, HasLen, 0)
	c.Check(s.Stdout(), Equals, "cba\n")
	c.Check(s.Stderr(), Equals, "")
}

func (s *confdbSuite) TestForbidWaitForWithNonConfdbGet(c *check.C) {
	s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
		err := fmt.Errorf("expected to get no requests (%v)", r)
		w.WriteHeader(500)
		fmt.Fprintf(w, `{"type": "error", "result": {"message": %q}}`, err)
		c.Error(err)
	})

	_, err := snapset.Parser(snapset.Client()).ParseArgs([]string{"get", ":foo", "--wait-for", "5s", "bar"})
	c.Assert(err, ErrorMatches, "cannot use --wait-for in non-confdb read")
	c.Check(s.Stdout(), Equals, "")
	c.Check(s.Stderr(), Equals, "")
}
