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

/*
 * Copyright (C) 2014-2021 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 (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"

	"gopkg.in/check.v1"

	"github.com/snapcore/snapd/client"
	"github.com/snapcore/snapd/daemon"
	"github.com/snapcore/snapd/interfaces"
	"github.com/snapcore/snapd/interfaces/builtin"
	"github.com/snapcore/snapd/interfaces/ifacetest"
	"github.com/snapcore/snapd/overlord/ifacestate"
	"github.com/snapcore/snapd/overlord/state"
)

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

type interfacesSuite struct {
	apiBaseSuite
}

func (s *interfacesSuite) SetUpTest(c *check.C) {
	s.apiBaseSuite.SetUpTest(c)

	s.expectWriteAccess(daemon.AuthenticatedAccess{Polkit: "io.snapcraft.snapd.manage-interfaces"})
}

func mockIface(c *check.C, d *daemon.Daemon, iface interfaces.Interface) {
	err := d.Overlord().InterfaceManager().Repository().AddInterface(iface)
	c.Assert(err, check.IsNil)
}

// inverseCaseMapper implements SnapMapper to use lower case internally and upper case externally.
type inverseCaseMapper struct {
	ifacestate.IdentityMapper // Embed the identity mapper to reuse empty state mapping functions.
}

func (m *inverseCaseMapper) RemapSnapFromRequest(snapName string) string {
	return strings.ToLower(snapName)
}

func (m *inverseCaseMapper) RemapSnapToResponse(snapName string) string {
	return strings.ToUpper(snapName)
}

func (m *inverseCaseMapper) SystemSnapName() string {
	return "core"
}

// Tests for POST /v2/interfaces

const (
	consumerYaml = `
name: consumer
version: 1
apps:
 app:
plugs:
 plug:
  interface: test
  key: value
  label: label
`

	producerYaml = `
name: producer
version: 1
apps:
 app:
slots:
 slot:
  interface: test
  key: value
  label: label
`

	coreProducerYaml = `
name: core
version: 1
slots:
 slot:
  interface: test
  key: value
  label: label
`

	differentProducerYaml = `
name: producer
version: 1
apps:
 app:
slots:
 slot:
  interface: different
  key: value
  label: label
`
)

func (s *interfacesSuite) TestConnectPlugSuccess(c *check.C) {
	restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
	defer restore()
	// Install an inverse case mapper to exercise the interface mapping at the same time.
	restore = ifacestate.MockSnapMapper(&inverseCaseMapper{})
	defer restore()

	d := s.daemon(c)

	s.mockSnap(c, consumerYaml)
	s.mockSnap(c, producerYaml)

	d.Overlord().Loop()
	defer d.Overlord().Stop()

	action := &client.InterfaceAction{
		Action: "connect",
		Plugs:  []client.Plug{{Snap: "CONSUMER", Name: "plug"}},
		Slots:  []client.Slot{{Snap: "PRODUCER", Name: "slot"}},
	}
	text, err := json.Marshal(action)
	c.Assert(err, check.IsNil)
	buf := bytes.NewBuffer(text)
	req, err := http.NewRequest("POST", "/v2/interfaces", buf)
	c.Assert(err, check.IsNil)
	rec := httptest.NewRecorder()
	s.req(c, req, nil, actionIsExpected).ServeHTTP(rec, req)
	c.Check(rec.Code, check.Equals, 202)
	var body map[string]any
	err = json.Unmarshal(rec.Body.Bytes(), &body)
	c.Check(err, check.IsNil)
	id := body["change"].(string)

	st := d.Overlord().State()
	st.Lock()
	chg := st.Change(id)
	st.Unlock()
	c.Assert(chg, check.NotNil)

	<-chg.Ready()

	st.Lock()
	err = chg.Err()
	st.Unlock()
	c.Assert(err, check.IsNil)

	repo := d.Overlord().InterfaceManager().Repository()
	ifaces := repo.Interfaces()
	c.Assert(ifaces.Connections, check.HasLen, 1)
	c.Check(ifaces.Connections, check.DeepEquals, []*interfaces.ConnRef{{
		PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"},
		SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"},
	}})
}

func (s *interfacesSuite) TestConnectPlugFailureInterfaceMismatch(c *check.C) {
	d := s.daemon(c)

	mockIface(c, d, &ifacetest.TestInterface{InterfaceName: "test"})
	mockIface(c, d, &ifacetest.TestInterface{InterfaceName: "different"})
	s.mockSnap(c, consumerYaml)
	s.mockSnap(c, differentProducerYaml)

	action := &client.InterfaceAction{
		Action: "connect",
		Plugs:  []client.Plug{{Snap: "consumer", Name: "plug"}},
		Slots:  []client.Slot{{Snap: "producer", Name: "slot"}},
	}
	text, err := json.Marshal(action)
	c.Assert(err, check.IsNil)
	buf := bytes.NewBuffer(text)
	req, err := http.NewRequest("POST", "/v2/interfaces", buf)
	c.Assert(err, check.IsNil)
	rec := httptest.NewRecorder()
	s.req(c, req, nil, actionIsExpected).ServeHTTP(rec, req)
	c.Check(rec.Code, check.Equals, 400)
	var body map[string]any
	err = json.Unmarshal(rec.Body.Bytes(), &body)
	c.Check(err, check.IsNil)
	c.Check(body, check.DeepEquals, map[string]any{
		"result": map[string]any{
			"message": "cannot connect consumer:plug (\"test\" interface) to producer:slot (\"different\" interface)",
		},
		"status":      "Bad Request",
		"status-code": 400.0,
		"type":        "error",
	})
	repo := d.Overlord().InterfaceManager().Repository()
	ifaces := repo.Interfaces()
	c.Assert(ifaces.Connections, check.HasLen, 0)
}

func (s *interfacesSuite) TestConnectPlugFailureNoSuchPlug(c *check.C) {
	d := s.daemon(c)

	mockIface(c, d, &ifacetest.TestInterface{InterfaceName: "test"})
	// there is no consumer, no plug defined
	s.mockSnap(c, producerYaml)
	s.mockSnap(c, consumerYaml)

	action := &client.InterfaceAction{
		Action: "connect",
		Plugs:  []client.Plug{{Snap: "consumer", Name: "missingplug"}},
		Slots:  []client.Slot{{Snap: "producer", Name: "slot"}},
	}
	text, err := json.Marshal(action)
	c.Assert(err, check.IsNil)
	buf := bytes.NewBuffer(text)
	req, err := http.NewRequest("POST", "/v2/interfaces", buf)
	c.Assert(err, check.IsNil)
	rec := httptest.NewRecorder()
	s.req(c, req, nil, actionIsExpected).ServeHTTP(rec, req)
	c.Check(rec.Code, check.Equals, 400)

	var body map[string]any
	err = json.Unmarshal(rec.Body.Bytes(), &body)
	c.Check(err, check.IsNil)
	c.Check(body, check.DeepEquals, map[string]any{
		"result": map[string]any{
			"message": "snap \"consumer\" has no plug named \"missingplug\"",
		},
		"status":      "Bad Request",
		"status-code": 400.0,
		"type":        "error",
	})

	repo := d.Overlord().InterfaceManager().Repository()
	ifaces := repo.Interfaces()
	c.Assert(ifaces.Connections, check.HasLen, 0)
}

func (s *interfacesSuite) TestConnectAlreadyConnected(c *check.C) {
	d := s.daemon(c)

	mockIface(c, d, &ifacetest.TestInterface{InterfaceName: "test"})
	// there is no consumer, no plug defined
	s.mockSnap(c, producerYaml)
	s.mockSnap(c, consumerYaml)

	repo := d.Overlord().InterfaceManager().Repository()
	connRef := &interfaces.ConnRef{
		PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"},
		SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"},
	}

	d.Overlord().Loop()
	defer d.Overlord().Stop()

	_, err := repo.Connect(connRef, nil, nil, nil, nil, nil)
	c.Assert(err, check.IsNil)
	conns := map[string]any{
		"consumer:plug producer:slot": map[string]any{
			"auto": false,
		},
	}
	st := d.Overlord().State()
	st.Lock()
	st.Set("conns", conns)
	st.Unlock()

	action := &client.InterfaceAction{
		Action: "connect",
		Plugs:  []client.Plug{{Snap: "consumer", Name: "plug"}},
		Slots:  []client.Slot{{Snap: "producer", Name: "slot"}},
	}
	text, err := json.Marshal(action)
	c.Assert(err, check.IsNil)
	buf := bytes.NewBuffer(text)
	req, err := http.NewRequest("POST", "/v2/interfaces", buf)
	c.Assert(err, check.IsNil)
	rec := httptest.NewRecorder()
	s.req(c, req, nil, actionIsExpected).ServeHTTP(rec, req)
	c.Check(rec.Code, check.Equals, 202)
	var body map[string]any
	err = json.Unmarshal(rec.Body.Bytes(), &body)
	c.Check(err, check.IsNil)
	id := body["change"].(string)

	st.Lock()
	chg := st.Change(id)
	c.Assert(chg.Tasks(), check.HasLen, 0)
	c.Assert(chg.Status(), check.Equals, state.DoneStatus)
	st.Unlock()
}

func (s *interfacesSuite) TestConnectPlugFailureNoSuchSlot(c *check.C) {
	d := s.daemon(c)

	mockIface(c, d, &ifacetest.TestInterface{InterfaceName: "test"})
	s.mockSnap(c, consumerYaml)
	s.mockSnap(c, producerYaml)
	// there is no producer, no slot defined

	action := &client.InterfaceAction{
		Action: "connect",
		Plugs:  []client.Plug{{Snap: "consumer", Name: "plug"}},
		Slots:  []client.Slot{{Snap: "producer", Name: "missingslot"}},
	}
	text, err := json.Marshal(action)
	c.Assert(err, check.IsNil)
	buf := bytes.NewBuffer(text)
	req, err := http.NewRequest("POST", "/v2/interfaces", buf)
	c.Assert(err, check.IsNil)
	rec := httptest.NewRecorder()
	s.req(c, req, nil, actionIsExpected).ServeHTTP(rec, req)
	c.Check(rec.Code, check.Equals, 400)

	var body map[string]any
	err = json.Unmarshal(rec.Body.Bytes(), &body)
	c.Check(err, check.IsNil)
	c.Check(body, check.DeepEquals, map[string]any{
		"result": map[string]any{
			"message": "snap \"producer\" has no slot named \"missingslot\"",
		},
		"status":      "Bad Request",
		"status-code": 400.0,
		"type":        "error",
	})

	repo := d.Overlord().InterfaceManager().Repository()
	ifaces := repo.Interfaces()
	c.Assert(ifaces.Connections, check.HasLen, 0)
}

func (s *interfacesSuite) testConnectFailureNoSnap(c *check.C, installedSnap string) {
	// validity, either consumer or producer needs to be enabled
	consumer := installedSnap == "consumer"
	producer := installedSnap == "producer"
	c.Assert(consumer || producer, check.Equals, true, check.Commentf("installed snap must be consumer or producer"))

	d := s.daemon(c)

	mockIface(c, d, &ifacetest.TestInterface{InterfaceName: "test"})

	if consumer {
		s.mockSnap(c, consumerYaml)
	}
	if producer {
		s.mockSnap(c, producerYaml)
	}

	action := &client.InterfaceAction{
		Action: "connect",
		Plugs:  []client.Plug{{Snap: "consumer", Name: "plug"}},
		Slots:  []client.Slot{{Snap: "producer", Name: "slot"}},
	}
	text, err := json.Marshal(action)
	c.Assert(err, check.IsNil)
	buf := bytes.NewBuffer(text)
	req, err := http.NewRequest("POST", "/v2/interfaces", buf)
	c.Assert(err, check.IsNil)
	rec := httptest.NewRecorder()
	s.req(c, req, nil, actionIsExpected).ServeHTTP(rec, req)
	c.Check(rec.Code, check.Equals, 400)

	var body map[string]any
	err = json.Unmarshal(rec.Body.Bytes(), &body)
	c.Check(err, check.IsNil)
	if producer {
		c.Check(body, check.DeepEquals, map[string]any{
			"result": map[string]any{
				"message": "snap \"consumer\" is not installed",
			},
			"status":      "Bad Request",
			"status-code": 400.0,
			"type":        "error",
		})
	} else {
		c.Check(body, check.DeepEquals, map[string]any{
			"result": map[string]any{
				"message": "snap \"producer\" is not installed",
			},
			"status":      "Bad Request",
			"status-code": 400.0,
			"type":        "error",
		})
	}
}

func (s *interfacesSuite) TestConnectPlugFailureNoPlugSnap(c *check.C) {
	s.testConnectFailureNoSnap(c, "producer")
}

func (s *interfacesSuite) TestConnectPlugFailureNoSlotSnap(c *check.C) {
	s.testConnectFailureNoSnap(c, "consumer")
}

func (s *interfacesSuite) TestConnectPlugChangeConflict(c *check.C) {
	d := s.daemon(c)

	mockIface(c, d, &ifacetest.TestInterface{InterfaceName: "test"})
	s.mockSnap(c, consumerYaml)
	s.mockSnap(c, producerYaml)
	// there is no producer, no slot defined

	s.simulateConflict("consumer")

	action := &client.InterfaceAction{
		Action: "connect",
		Plugs:  []client.Plug{{Snap: "consumer", Name: "plug"}},
		Slots:  []client.Slot{{Snap: "producer", Name: "slot"}},
	}
	text, err := json.Marshal(action)
	c.Assert(err, check.IsNil)
	buf := bytes.NewBuffer(text)
	req, err := http.NewRequest("POST", "/v2/interfaces", buf)
	c.Assert(err, check.IsNil)
	rec := httptest.NewRecorder()
	s.req(c, req, nil, actionIsExpected).ServeHTTP(rec, req)
	c.Check(rec.Code, check.Equals, 409)

	var body map[string]any
	err = json.Unmarshal(rec.Body.Bytes(), &body)
	c.Check(err, check.IsNil)
	c.Check(body, check.DeepEquals, map[string]any{
		"status-code": 409.,
		"status":      "Conflict",
		"result": map[string]any{
			"message": `snap "consumer" has "manip" change in progress`,
			"kind":    "snap-change-conflict",
			"value": map[string]any{
				"change-kind": "manip",
				"snap-name":   "consumer",
			},
		},
		"type": "error"})
}

func (s *interfacesSuite) TestConnectCoreSystemAlias(c *check.C) {
	revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
	defer revert()
	d := s.daemon(c)

	s.mockSnap(c, consumerYaml)
	s.mockSnap(c, coreProducerYaml)

	d.Overlord().Loop()
	defer d.Overlord().Stop()

	action := &client.InterfaceAction{
		Action: "connect",
		Plugs:  []client.Plug{{Snap: "consumer", Name: "plug"}},
		Slots:  []client.Slot{{Snap: "system", Name: "slot"}},
	}
	text, err := json.Marshal(action)
	c.Assert(err, check.IsNil)
	buf := bytes.NewBuffer(text)
	req, err := http.NewRequest("POST", "/v2/interfaces", buf)
	c.Assert(err, check.IsNil)
	rec := httptest.NewRecorder()
	s.req(c, req, nil, actionIsExpected).ServeHTTP(rec, req)
	c.Check(rec.Code, check.Equals, 202)
	var body map[string]any
	err = json.Unmarshal(rec.Body.Bytes(), &body)
	c.Check(err, check.IsNil)
	id := body["change"].(string)

	st := d.Overlord().State()
	st.Lock()
	chg := st.Change(id)
	st.Unlock()
	c.Assert(chg, check.NotNil)

	<-chg.Ready()

	st.Lock()
	err = chg.Err()
	st.Unlock()
	c.Assert(err, check.IsNil)

	repo := d.Overlord().InterfaceManager().Repository()
	ifaces := repo.Interfaces()
	c.Assert(ifaces.Connections, check.HasLen, 1)
	c.Check(ifaces.Connections, check.DeepEquals, []*interfaces.ConnRef{{
		PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"},
		SlotRef: interfaces.SlotRef{Snap: "core", Name: "slot"}}})
}

func (s *interfacesSuite) testDisconnect(c *check.C, plugSnap, plugName, slotSnap, slotName string) {
	restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
	defer restore()
	// Install an inverse case mapper to exercise the interface mapping at the same time.
	restore = ifacestate.MockSnapMapper(&inverseCaseMapper{})
	defer restore()
	d := s.daemon(c)

	s.mockSnap(c, consumerYaml)
	s.mockSnap(c, producerYaml)

	repo := d.Overlord().InterfaceManager().Repository()
	connRef := &interfaces.ConnRef{
		PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"},
		SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"},
	}
	_, err := repo.Connect(connRef, nil, nil, nil, nil, nil)
	c.Assert(err, check.IsNil)

	st := d.Overlord().State()
	st.Lock()
	st.Set("conns", map[string]any{
		"consumer:plug producer:slot": map[string]any{
			"interface": "test",
		},
	})
	st.Unlock()

	d.Overlord().Loop()
	defer d.Overlord().Stop()

	action := &client.InterfaceAction{
		Action: "disconnect",
		Plugs:  []client.Plug{{Snap: plugSnap, Name: plugName}},
		Slots:  []client.Slot{{Snap: slotSnap, Name: slotName}},
	}
	text, err := json.Marshal(action)
	c.Assert(err, check.IsNil)
	buf := bytes.NewBuffer(text)
	req, err := http.NewRequest("POST", "/v2/interfaces", buf)
	c.Assert(err, check.IsNil)
	rec := httptest.NewRecorder()
	s.req(c, req, nil, actionIsExpected).ServeHTTP(rec, req)
	c.Check(rec.Code, check.Equals, 202)
	var body map[string]any
	err = json.Unmarshal(rec.Body.Bytes(), &body)
	c.Check(err, check.IsNil)
	id := body["change"].(string)

	st.Lock()
	chg := st.Change(id)
	st.Unlock()
	c.Assert(chg, check.NotNil)

	<-chg.Ready()

	st.Lock()
	err = chg.Err()
	st.Unlock()
	c.Assert(err, check.IsNil)

	ifaces := repo.Interfaces()
	c.Assert(ifaces.Connections, check.HasLen, 0)
}

func (s *interfacesSuite) TestDisconnectPlugSuccess(c *check.C) {
	s.testDisconnect(c, "CONSUMER", "plug", "PRODUCER", "slot")
}

func (s *interfacesSuite) TestDisconnectPlugSuccessWithEmptyPlug(c *check.C) {
	s.testDisconnect(c, "", "", "PRODUCER", "slot")
}

func (s *interfacesSuite) TestDisconnectPlugSuccessWithEmptySlot(c *check.C) {
	s.testDisconnect(c, "CONSUMER", "plug", "", "")
}

func (s *interfacesSuite) TestDisconnectPlugFailureNoSuchPlug(c *check.C) {
	revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
	defer revert()
	s.daemon(c)

	s.mockSnap(c, consumerYaml)
	s.mockSnap(c, producerYaml)

	action := &client.InterfaceAction{
		Action: "disconnect",
		Plugs:  []client.Plug{{Snap: "consumer", Name: "missingplug"}},
		Slots:  []client.Slot{{Snap: "producer", Name: "slot"}},
	}
	text, err := json.Marshal(action)
	c.Assert(err, check.IsNil)
	buf := bytes.NewBuffer(text)
	req, err := http.NewRequest("POST", "/v2/interfaces", buf)
	c.Assert(err, check.IsNil)
	rec := httptest.NewRecorder()
	s.req(c, req, nil, actionIsExpected).ServeHTTP(rec, req)
	c.Check(rec.Code, check.Equals, 400)
	var body map[string]any
	err = json.Unmarshal(rec.Body.Bytes(), &body)
	c.Check(err, check.IsNil)
	c.Check(body, check.DeepEquals, map[string]any{
		"result": map[string]any{
			"message": "snap \"consumer\" has no plug named \"missingplug\"",
		},
		"status":      "Bad Request",
		"status-code": 400.0,
		"type":        "error",
	})
}

func (s *interfacesSuite) testDisconnectFailureNoSnap(c *check.C, installedSnap string) {
	// validity, either consumer or producer needs to be enabled
	consumer := installedSnap == "consumer"
	producer := installedSnap == "producer"
	c.Assert(consumer || producer, check.Equals, true, check.Commentf("installed snap must be consumer or producer"))

	revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
	defer revert()
	s.daemon(c)

	if consumer {
		s.mockSnap(c, consumerYaml)
	}
	if producer {
		s.mockSnap(c, producerYaml)
	}

	action := &client.InterfaceAction{
		Action: "disconnect",
		Plugs:  []client.Plug{{Snap: "consumer", Name: "plug"}},
		Slots:  []client.Slot{{Snap: "producer", Name: "slot"}},
	}
	text, err := json.Marshal(action)
	c.Assert(err, check.IsNil)
	buf := bytes.NewBuffer(text)
	req, err := http.NewRequest("POST", "/v2/interfaces", buf)
	c.Assert(err, check.IsNil)
	rec := httptest.NewRecorder()
	s.req(c, req, nil, actionIsExpected).ServeHTTP(rec, req)
	c.Check(rec.Code, check.Equals, 400)
	var body map[string]any
	err = json.Unmarshal(rec.Body.Bytes(), &body)
	c.Check(err, check.IsNil)

	if producer {
		c.Check(body, check.DeepEquals, map[string]any{
			"result": map[string]any{
				"message": "snap \"consumer\" is not installed",
			},
			"status":      "Bad Request",
			"status-code": 400.0,
			"type":        "error",
		})
	} else {
		c.Check(body, check.DeepEquals, map[string]any{
			"result": map[string]any{
				"message": "snap \"producer\" is not installed",
			},
			"status":      "Bad Request",
			"status-code": 400.0,
			"type":        "error",
		})
	}
}

func (s *interfacesSuite) TestDisconnectPlugFailureNoPlugSnap(c *check.C) {
	s.testDisconnectFailureNoSnap(c, "producer")
}

func (s *interfacesSuite) TestDisconnectPlugFailureNoSlotSnap(c *check.C) {
	s.testDisconnectFailureNoSnap(c, "consumer")
}

func (s *interfacesSuite) TestDisconnectPlugNothingToDo(c *check.C) {
	revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
	defer revert()
	s.daemon(c)

	s.mockSnap(c, consumerYaml)
	s.mockSnap(c, producerYaml)

	action := &client.InterfaceAction{
		Action: "disconnect",
		Plugs:  []client.Plug{{Snap: "consumer", Name: "plug"}},
		Slots:  []client.Slot{{Snap: "", Name: ""}},
	}
	text, err := json.Marshal(action)
	c.Assert(err, check.IsNil)
	buf := bytes.NewBuffer(text)
	req, err := http.NewRequest("POST", "/v2/interfaces", buf)
	c.Assert(err, check.IsNil)
	rec := httptest.NewRecorder()
	s.req(c, req, nil, actionIsExpected).ServeHTTP(rec, req)
	c.Check(rec.Code, check.Equals, 400)
	var body map[string]any
	err = json.Unmarshal(rec.Body.Bytes(), &body)
	c.Check(err, check.IsNil)
	c.Check(body, check.DeepEquals, map[string]any{
		"result": map[string]any{
			"message": "nothing to do",
			"kind":    "interfaces-unchanged",
		},
		"status":      "Bad Request",
		"status-code": 400.0,
		"type":        "error",
	})
}

func (s *interfacesSuite) TestDisconnectPlugFailureNoSuchSlot(c *check.C) {
	revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
	defer revert()
	s.daemon(c)

	s.mockSnap(c, consumerYaml)
	s.mockSnap(c, producerYaml)

	action := &client.InterfaceAction{
		Action: "disconnect",
		Plugs:  []client.Plug{{Snap: "consumer", Name: "plug"}},
		Slots:  []client.Slot{{Snap: "producer", Name: "missingslot"}},
	}
	text, err := json.Marshal(action)
	c.Assert(err, check.IsNil)
	buf := bytes.NewBuffer(text)
	req, err := http.NewRequest("POST", "/v2/interfaces", buf)
	c.Assert(err, check.IsNil)
	rec := httptest.NewRecorder()
	s.req(c, req, nil, actionIsExpected).ServeHTTP(rec, req)

	c.Check(rec.Code, check.Equals, 400)
	var body map[string]any
	err = json.Unmarshal(rec.Body.Bytes(), &body)
	c.Check(err, check.IsNil)
	c.Check(body, check.DeepEquals, map[string]any{
		"result": map[string]any{
			"message": "snap \"producer\" has no slot named \"missingslot\"",
		},
		"status":      "Bad Request",
		"status-code": 400.0,
		"type":        "error",
	})
}

func (s *interfacesSuite) TestDisconnectPlugFailureNotConnected(c *check.C) {
	revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
	defer revert()
	s.daemon(c)

	s.mockSnap(c, consumerYaml)
	s.mockSnap(c, producerYaml)

	action := &client.InterfaceAction{
		Action: "disconnect",
		Plugs:  []client.Plug{{Snap: "consumer", Name: "plug"}},
		Slots:  []client.Slot{{Snap: "producer", Name: "slot"}},
	}
	text, err := json.Marshal(action)
	c.Assert(err, check.IsNil)
	buf := bytes.NewBuffer(text)
	req, err := http.NewRequest("POST", "/v2/interfaces", buf)
	c.Assert(err, check.IsNil)
	rec := httptest.NewRecorder()
	s.req(c, req, nil, actionIsExpected).ServeHTTP(rec, req)

	c.Check(rec.Code, check.Equals, 400)
	var body map[string]any
	err = json.Unmarshal(rec.Body.Bytes(), &body)
	c.Check(err, check.IsNil)
	c.Check(body, check.DeepEquals, map[string]any{
		"result": map[string]any{
			"message": "cannot disconnect consumer:plug from producer:slot, it is not connected",
		},
		"status":      "Bad Request",
		"status-code": 400.0,
		"type":        "error",
	})
}

func (s *interfacesSuite) TestDisconnectForgetPlugFailureNotConnected(c *check.C) {
	revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
	defer revert()
	s.daemon(c)

	s.mockSnap(c, consumerYaml)
	s.mockSnap(c, producerYaml)

	action := &client.InterfaceAction{
		Action: "disconnect",
		Forget: true,
		Plugs:  []client.Plug{{Snap: "consumer", Name: "plug"}},
		Slots:  []client.Slot{{Snap: "producer", Name: "slot"}},
	}
	text, err := json.Marshal(action)
	c.Assert(err, check.IsNil)
	buf := bytes.NewBuffer(text)
	req, err := http.NewRequest("POST", "/v2/interfaces", buf)
	c.Assert(err, check.IsNil)
	rec := httptest.NewRecorder()
	s.req(c, req, nil, actionIsExpected).ServeHTTP(rec, req)

	c.Check(rec.Code, check.Equals, 400)
	var body map[string]any
	err = json.Unmarshal(rec.Body.Bytes(), &body)
	c.Check(err, check.IsNil)
	c.Check(body, check.DeepEquals, map[string]any{
		"result": map[string]any{
			"message": "cannot forget connection consumer:plug from producer:slot, it was not connected",
		},
		"status":      "Bad Request",
		"status-code": 400.0,
		"type":        "error",
	})
}

func (s *interfacesSuite) TestDisconnectConflict(c *check.C) {
	revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
	defer revert()
	d := s.daemon(c)

	s.mockSnap(c, consumerYaml)
	s.mockSnap(c, producerYaml)

	repo := d.Overlord().InterfaceManager().Repository()
	connRef := &interfaces.ConnRef{
		PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"},
		SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"},
	}
	_, err := repo.Connect(connRef, nil, nil, nil, nil, nil)
	c.Assert(err, check.IsNil)

	st := d.Overlord().State()
	st.Lock()
	st.Set("conns", map[string]any{
		"consumer:plug producer:slot": map[string]any{
			"interface": "test",
		},
	})
	st.Unlock()

	s.simulateConflict("consumer")

	action := &client.InterfaceAction{
		Action: "disconnect",
		Plugs:  []client.Plug{{Snap: "consumer", Name: "plug"}},
		Slots:  []client.Slot{{Snap: "producer", Name: "slot"}},
	}
	text, err := json.Marshal(action)
	c.Assert(err, check.IsNil)
	buf := bytes.NewBuffer(text)
	req, err := http.NewRequest("POST", "/v2/interfaces", buf)
	c.Assert(err, check.IsNil)
	rec := httptest.NewRecorder()
	s.req(c, req, nil, actionIsExpected).ServeHTTP(rec, req)

	c.Check(rec.Code, check.Equals, 409)

	var body map[string]any
	err = json.Unmarshal(rec.Body.Bytes(), &body)
	c.Check(err, check.IsNil)
	c.Check(body, check.DeepEquals, map[string]any{
		"status-code": 409.,
		"status":      "Conflict",
		"result": map[string]any{
			"message": `snap "consumer" has "manip" change in progress`,
			"kind":    "snap-change-conflict",
			"value": map[string]any{
				"change-kind": "manip",
				"snap-name":   "consumer",
			},
		},
		"type": "error"})
}

func (s *interfacesSuite) TestDisconnectCoreSystemAlias(c *check.C) {
	revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
	defer revert()
	d := s.daemon(c)

	s.mockSnap(c, consumerYaml)
	s.mockSnap(c, coreProducerYaml)

	repo := d.Overlord().InterfaceManager().Repository()
	connRef := &interfaces.ConnRef{
		PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"},
		SlotRef: interfaces.SlotRef{Snap: "core", Name: "slot"},
	}
	_, err := repo.Connect(connRef, nil, nil, nil, nil, nil)
	c.Assert(err, check.IsNil)

	st := d.Overlord().State()
	st.Lock()
	st.Set("conns", map[string]any{
		"consumer:plug core:slot": map[string]any{
			"interface": "test",
		},
	})
	st.Unlock()

	d.Overlord().Loop()
	defer d.Overlord().Stop()

	action := &client.InterfaceAction{
		Action: "disconnect",
		Plugs:  []client.Plug{{Snap: "consumer", Name: "plug"}},
		Slots:  []client.Slot{{Snap: "system", Name: "slot"}},
	}
	text, err := json.Marshal(action)
	c.Assert(err, check.IsNil)
	buf := bytes.NewBuffer(text)
	req, err := http.NewRequest("POST", "/v2/interfaces", buf)
	c.Assert(err, check.IsNil)
	rec := httptest.NewRecorder()
	s.req(c, req, nil, actionIsExpected).ServeHTTP(rec, req)
	c.Check(rec.Code, check.Equals, 202)
	var body map[string]any
	err = json.Unmarshal(rec.Body.Bytes(), &body)
	c.Check(err, check.IsNil)
	id := body["change"].(string)

	st.Lock()
	chg := st.Change(id)
	st.Unlock()
	c.Assert(chg, check.NotNil)

	<-chg.Ready()

	st.Lock()
	err = chg.Err()
	st.Unlock()
	c.Assert(err, check.IsNil)

	ifaces := repo.Interfaces()
	c.Assert(ifaces.Connections, check.HasLen, 0)
}

func (s *interfacesSuite) TestUnsupportedInterfaceRequest(c *check.C) {
	s.daemon(c)
	buf := bytes.NewBuffer([]byte(`garbage`))
	req, err := http.NewRequest("POST", "/v2/interfaces", buf)
	c.Assert(err, check.IsNil)
	rec := httptest.NewRecorder()
	s.req(c, req, nil, actionIsExpected).ServeHTTP(rec, req)
	c.Check(rec.Code, check.Equals, 400)
	var body map[string]any
	err = json.Unmarshal(rec.Body.Bytes(), &body)
	c.Check(err, check.IsNil)
	c.Check(body, check.DeepEquals, map[string]any{
		"result": map[string]any{
			"message": "cannot decode request body into an interface action: invalid character 'g' looking for beginning of value",
		},
		"status":      "Bad Request",
		"status-code": 400.0,
		"type":        "error",
	})
}

func (s *interfacesSuite) TestMissingInterfaceAction(c *check.C) {
	s.daemon(c)
	action := &client.InterfaceAction{}
	text, err := json.Marshal(action)
	c.Assert(err, check.IsNil)
	buf := bytes.NewBuffer(text)
	req, err := http.NewRequest("POST", "/v2/interfaces", buf)
	c.Assert(err, check.IsNil)
	rec := httptest.NewRecorder()
	s.req(c, req, nil, actionIsExpected).ServeHTTP(rec, req)
	c.Check(rec.Code, check.Equals, 400)
	var body map[string]any
	err = json.Unmarshal(rec.Body.Bytes(), &body)
	c.Check(err, check.IsNil)
	c.Check(body, check.DeepEquals, map[string]any{
		"result": map[string]any{
			"message": "interface action not specified",
		},
		"status":      "Bad Request",
		"status-code": 400.0,
		"type":        "error",
	})
}

func (s *interfacesSuite) TestUnsupportedInterfaceAction(c *check.C) {
	s.daemon(c)
	action := &client.InterfaceAction{Action: "foo"}
	text, err := json.Marshal(action)
	c.Assert(err, check.IsNil)
	buf := bytes.NewBuffer(text)
	req, err := http.NewRequest("POST", "/v2/interfaces", buf)
	c.Assert(err, check.IsNil)
	rec := httptest.NewRecorder()
	s.req(c, req, nil, actionIsUnexpected).ServeHTTP(rec, req)
	c.Check(rec.Code, check.Equals, 400)
	var body map[string]any
	err = json.Unmarshal(rec.Body.Bytes(), &body)
	c.Check(err, check.IsNil)
	c.Check(body, check.DeepEquals, map[string]any{
		"result": map[string]any{
			"message": "unsupported interface action: \"foo\"",
		},
		"status":      "Bad Request",
		"status-code": 400.0,
		"type":        "error",
	})
}

// Tests for GET /v2/interfaces

func (s *interfacesSuite) TestInterfacesLegacy(c *check.C) {
	restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
	defer restore()
	// Install an inverse case mapper to exercise the interface mapping at the same time.
	restore = ifacestate.MockSnapMapper(&inverseCaseMapper{})
	defer restore()

	d := s.daemon(c)

	var anotherConsumerYaml = `
name: another-consumer-%s
version: 1
apps:
 app:
plugs:
 plug:
  interface: test
  key: value
  label: label
`
	s.mockSnap(c, consumerYaml)
	s.mockSnap(c, fmt.Sprintf(anotherConsumerYaml, "def"))
	s.mockSnap(c, fmt.Sprintf(anotherConsumerYaml, "abc"))
	s.mockSnap(c, producerYaml)

	repo := d.Overlord().InterfaceManager().Repository()
	connRef := &interfaces.ConnRef{
		PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"},
		SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"},
	}
	_, err := repo.Connect(connRef, nil, nil, nil, nil, nil)
	c.Assert(err, check.IsNil)

	st := d.Overlord().State()
	st.Lock()
	st.Set("conns", map[string]any{
		"consumer:plug producer:slot": map[string]any{
			"interface": "test",
			"auto":      true,
		},
		"another-consumer-def:plug producer:slot": map[string]any{
			"interface": "test",
			"by-gadget": true,
			"auto":      true,
		},
		"another-consumer-abc:plug producer:slot": map[string]any{
			"interface": "test",
			"by-gadget": true,
			"auto":      true,
		},
	})
	st.Unlock()

	req, err := http.NewRequest("GET", "/v2/interfaces", nil)
	c.Assert(err, check.IsNil)
	rec := httptest.NewRecorder()
	s.req(c, req, nil, actionIsExpected).ServeHTTP(rec, req)
	c.Check(rec.Code, check.Equals, 200)
	var body map[string]any
	err = json.Unmarshal(rec.Body.Bytes(), &body)
	c.Check(err, check.IsNil)
	c.Check(body, check.DeepEquals, map[string]any{
		"result": map[string]any{
			"plugs": []any{
				map[string]any{
					"snap":      "another-consumer-abc",
					"plug":      "plug",
					"interface": "test",
					"attrs":     map[string]any{"key": "value"},
					"apps":      []any{"app"},
					"label":     "label",
					"connections": []any{
						map[string]any{"snap": "producer", "slot": "slot"},
					},
				},
				map[string]any{
					"snap":      "another-consumer-def",
					"plug":      "plug",
					"interface": "test",
					"attrs":     map[string]any{"key": "value"},
					"apps":      []any{"app"},
					"label":     "label",
					"connections": []any{
						map[string]any{"snap": "producer", "slot": "slot"},
					},
				},
				map[string]any{
					"snap":      "consumer",
					"plug":      "plug",
					"interface": "test",
					"attrs":     map[string]any{"key": "value"},
					"apps":      []any{"app"},
					"label":     "label",
					"connections": []any{
						map[string]any{"snap": "producer", "slot": "slot"},
					},
				},
			},
			"slots": []any{
				map[string]any{
					"snap":      "producer",
					"slot":      "slot",
					"interface": "test",
					"attrs":     map[string]any{"key": "value"},
					"apps":      []any{"app"},
					"label":     "label",
					"connections": []any{
						map[string]any{"snap": "another-consumer-abc", "plug": "plug"},
						map[string]any{"snap": "another-consumer-def", "plug": "plug"},
						map[string]any{"snap": "consumer", "plug": "plug"},
					},
				},
			},
		},
		"status":      "OK",
		"status-code": 200.0,
		"type":        "sync",
	})
}

func (s *interfacesSuite) TestInterfacesModern(c *check.C) {
	restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
	defer restore()
	// Install an inverse case mapper to exercise the interface mapping at the same time.
	restore = ifacestate.MockSnapMapper(&inverseCaseMapper{})
	defer restore()

	d := s.daemon(c)

	s.mockSnap(c, consumerYaml)
	s.mockSnap(c, producerYaml)

	repo := d.Overlord().InterfaceManager().Repository()
	connRef := &interfaces.ConnRef{
		PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"},
		SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"},
	}
	_, err := repo.Connect(connRef, nil, nil, nil, nil, nil)
	c.Assert(err, check.IsNil)

	req, err := http.NewRequest("GET", "/v2/interfaces?select=connected&doc=true&plugs=true&slots=true", nil)
	c.Assert(err, check.IsNil)
	rec := httptest.NewRecorder()
	s.req(c, req, nil, actionIsExpected).ServeHTTP(rec, req)
	c.Check(rec.Code, check.Equals, 200)
	var body map[string]any
	err = json.Unmarshal(rec.Body.Bytes(), &body)
	c.Check(err, check.IsNil)
	c.Check(body, check.DeepEquals, map[string]any{
		"result": []any{
			map[string]any{
				"name":    "test",
				"doc-url": "https://snapcraft.io/docs/test-interface",
				"plugs": []any{
					map[string]any{
						"snap":  "consumer",
						"plug":  "plug",
						"label": "label",
						"attrs": map[string]any{
							"key": "value",
						},
					}},
				"slots": []any{
					map[string]any{
						"snap":  "producer",
						"slot":  "slot",
						"label": "label",
						"attrs": map[string]any{
							"key": "value",
						},
					},
				},
			},
		},
		"status":      "OK",
		"status-code": 200.0,
		"type":        "sync",
	})
}

func (s *interfacesSuite) TestInterfacesAllDefaultDocURL(c *check.C) {
	_ = s.daemon(c)

	req, err := http.NewRequest("GET", "/v2/interfaces?select=all&doc=true", nil)
	c.Assert(err, check.IsNil)
	rec := httptest.NewRecorder()
	s.req(c, req, nil, actionIsExpected).ServeHTTP(rec, req)
	c.Check(rec.Code, check.Equals, 200)
	var body map[string]any
	err = json.Unmarshal(rec.Body.Bytes(), &body)
	c.Check(err, check.IsNil)

	if result, ok := body["result"].([]any); ok {
		for _, content := range result {
			if contentMap, ok := content.(map[string]any); ok {
				name := contentMap["name"].(string)
				summary := contentMap["summary"].(string)
				docURL := contentMap["doc-url"].(string)
				c.Check(summary, check.Not(check.Equals), "", check.Commentf("interface: %s summary should not be empty", name))
				c.Check(docURL, check.Equals, fmt.Sprintf("https://snapcraft.io/docs/%s-interface", name), check.Commentf("interface: %s should use default doc URL", name))
			}
		}
	}
}
