// Copyright 2024 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package service_test

import (
	"context"
	"fmt"
	"slices"
	"strings"
	"testing"

	"github.com/dennwc/iters"
	"github.com/livekit/protocol/livekit"
	"github.com/livekit/protocol/utils"
	"github.com/livekit/protocol/utils/guid"
	"github.com/stretchr/testify/require"
	"google.golang.org/protobuf/proto"

	"github.com/livekit/livekit-server/pkg/service"
)

func TestSIPStoreDispatch(t *testing.T) {
	ctx := context.Background()
	rs := redisStoreDocker(t)

	id := guid.New(utils.SIPDispatchRulePrefix)

	// No dispatch rules initially.
	list, err := rs.ListSIPDispatchRule(ctx, &livekit.ListSIPDispatchRuleRequest{})
	require.NoError(t, err)
	require.Empty(t, list.Items)

	// Loading non-existent dispatch should return proper not found error.
	got, err := rs.LoadSIPDispatchRule(ctx, id)
	require.Equal(t, service.ErrSIPDispatchRuleNotFound, err)
	require.Nil(t, got)

	// Creation without ID should fail.
	rule := &livekit.SIPDispatchRuleInfo{
		TrunkIds: []string{"trunk"},
		Rule: &livekit.SIPDispatchRule{Rule: &livekit.SIPDispatchRule_DispatchRuleDirect{
			DispatchRuleDirect: &livekit.SIPDispatchRuleDirect{
				RoomName: "room",
				Pin:      "1234",
			},
		}},
	}
	err = rs.StoreSIPDispatchRule(ctx, rule)
	require.Error(t, err)

	// Creation
	rule.SipDispatchRuleId = id
	err = rs.StoreSIPDispatchRule(ctx, rule)
	require.NoError(t, err)

	// Loading
	got, err = rs.LoadSIPDispatchRule(ctx, id)
	require.NoError(t, err)
	require.True(t, proto.Equal(rule, got))

	// Listing
	list, err = rs.ListSIPDispatchRule(ctx, &livekit.ListSIPDispatchRuleRequest{})
	require.NoError(t, err)
	require.Len(t, list.Items, 1)
	require.True(t, proto.Equal(rule, list.Items[0]))

	// Deletion. Should not return error if not exists.
	err = rs.DeleteSIPDispatchRule(ctx, id)
	require.NoError(t, err)
	err = rs.DeleteSIPDispatchRule(ctx, id)
	require.NoError(t, err)

	// Check that it's deleted.
	list, err = rs.ListSIPDispatchRule(ctx, &livekit.ListSIPDispatchRuleRequest{})
	require.NoError(t, err)
	require.Empty(t, list.Items)

	got, err = rs.LoadSIPDispatchRule(ctx, id)
	require.Equal(t, service.ErrSIPDispatchRuleNotFound, err)
	require.Nil(t, got)
}

func TestSIPStoreTrunk(t *testing.T) {
	ctx := context.Background()
	rs := redisStoreDocker(t)

	oldID := guid.New(utils.SIPTrunkPrefix)
	inID := guid.New(utils.SIPTrunkPrefix)
	outID := guid.New(utils.SIPTrunkPrefix)

	// No trunks initially. Check legacy, inbound, outbound.
	// Loading non-existent trunk should return proper not found error.
	oldList, err := rs.ListSIPTrunk(ctx, &livekit.ListSIPTrunkRequest{})
	require.NoError(t, err)
	require.Empty(t, oldList.Items)

	old, err := rs.LoadSIPTrunk(ctx, oldID)
	require.Equal(t, service.ErrSIPTrunkNotFound, err)
	require.Nil(t, old)

	inList, err := rs.ListSIPInboundTrunk(ctx, &livekit.ListSIPInboundTrunkRequest{})
	require.NoError(t, err)
	require.Empty(t, inList.Items)

	in, err := rs.LoadSIPInboundTrunk(ctx, oldID)
	require.Equal(t, service.ErrSIPTrunkNotFound, err)
	require.Nil(t, in)

	outList, err := rs.ListSIPOutboundTrunk(ctx, &livekit.ListSIPOutboundTrunkRequest{})
	require.NoError(t, err)
	require.Empty(t, outList.Items)

	out, err := rs.LoadSIPOutboundTrunk(ctx, oldID)
	require.Equal(t, service.ErrSIPTrunkNotFound, err)
	require.Nil(t, out)

	// Creation without ID should fail.
	oldT := &livekit.SIPTrunkInfo{
		Name: "Legacy",
	}
	err = rs.StoreSIPTrunk(ctx, oldT)
	require.Error(t, err)

	inT := &livekit.SIPInboundTrunkInfo{
		Name: "Inbound",
	}
	err = rs.StoreSIPInboundTrunk(ctx, inT)
	require.Error(t, err)

	outT := &livekit.SIPOutboundTrunkInfo{
		Name: "Outbound",
	}
	err = rs.StoreSIPOutboundTrunk(ctx, outT)
	require.Error(t, err)

	// Creation
	oldT.SipTrunkId = oldID
	err = rs.StoreSIPTrunk(ctx, oldT)
	require.NoError(t, err)

	inT.SipTrunkId = inID
	err = rs.StoreSIPInboundTrunk(ctx, inT)
	require.NoError(t, err)

	outT.SipTrunkId = outID
	err = rs.StoreSIPOutboundTrunk(ctx, outT)
	require.NoError(t, err)

	// Loading (with matching kind)
	oldT2, err := rs.LoadSIPTrunk(ctx, oldID)
	require.NoError(t, err)
	require.True(t, proto.Equal(oldT, oldT2))

	inT2, err := rs.LoadSIPInboundTrunk(ctx, inID)
	require.NoError(t, err)
	require.True(t, proto.Equal(inT, inT2))

	outT2, err := rs.LoadSIPOutboundTrunk(ctx, outID)
	require.NoError(t, err)
	require.True(t, proto.Equal(outT, outT2))

	// Loading (compat)
	oldT2, err = rs.LoadSIPTrunk(ctx, inID)
	require.NoError(t, err)
	require.True(t, proto.Equal(inT.AsTrunkInfo(), oldT2))

	oldT2, err = rs.LoadSIPTrunk(ctx, outID)
	require.NoError(t, err)
	require.True(t, proto.Equal(outT.AsTrunkInfo(), oldT2))

	inT2, err = rs.LoadSIPInboundTrunk(ctx, oldID)
	require.NoError(t, err)
	require.True(t, proto.Equal(oldT.AsInbound(), inT2))

	outT2, err = rs.LoadSIPOutboundTrunk(ctx, oldID)
	require.NoError(t, err)
	require.True(t, proto.Equal(oldT.AsOutbound(), outT2))

	// Listing (always shows legacy + new)
	listOld, err := rs.ListSIPTrunk(ctx, &livekit.ListSIPTrunkRequest{})
	require.NoError(t, err)
	require.Len(t, listOld.Items, 3)
	slices.SortFunc(listOld.Items, func(a, b *livekit.SIPTrunkInfo) int {
		return strings.Compare(a.Name, b.Name)
	})
	require.True(t, proto.Equal(inT.AsTrunkInfo(), listOld.Items[0]))
	require.True(t, proto.Equal(oldT, listOld.Items[1]))
	require.True(t, proto.Equal(outT.AsTrunkInfo(), listOld.Items[2]))

	listIn, err := rs.ListSIPInboundTrunk(ctx, &livekit.ListSIPInboundTrunkRequest{})
	require.NoError(t, err)
	require.Len(t, listIn.Items, 2)
	slices.SortFunc(listIn.Items, func(a, b *livekit.SIPInboundTrunkInfo) int {
		return strings.Compare(a.Name, b.Name)
	})
	require.True(t, proto.Equal(inT, listIn.Items[0]))
	require.True(t, proto.Equal(oldT.AsInbound(), listIn.Items[1]))

	listOut, err := rs.ListSIPOutboundTrunk(ctx, &livekit.ListSIPOutboundTrunkRequest{})
	require.NoError(t, err)
	require.Len(t, listOut.Items, 2)
	slices.SortFunc(listOut.Items, func(a, b *livekit.SIPOutboundTrunkInfo) int {
		return strings.Compare(a.Name, b.Name)
	})
	require.True(t, proto.Equal(oldT.AsOutbound(), listOut.Items[0]))
	require.True(t, proto.Equal(outT, listOut.Items[1]))

	// Deletion. Should not return error if not exists.
	err = rs.DeleteSIPTrunk(ctx, oldID)
	require.NoError(t, err)
	err = rs.DeleteSIPTrunk(ctx, oldID)
	require.NoError(t, err)

	// Other objects are still there.
	inT2, err = rs.LoadSIPInboundTrunk(ctx, inID)
	require.NoError(t, err)
	require.True(t, proto.Equal(inT, inT2))

	outT2, err = rs.LoadSIPOutboundTrunk(ctx, outID)
	require.NoError(t, err)
	require.True(t, proto.Equal(outT, outT2))

	// Delete the rest
	err = rs.DeleteSIPTrunk(ctx, inID)
	require.NoError(t, err)
	err = rs.DeleteSIPTrunk(ctx, outID)
	require.NoError(t, err)

	// Check everything is deleted.
	oldList, err = rs.ListSIPTrunk(ctx, &livekit.ListSIPTrunkRequest{})
	require.NoError(t, err)
	require.Empty(t, oldList.Items)

	inList, err = rs.ListSIPInboundTrunk(ctx, &livekit.ListSIPInboundTrunkRequest{})
	require.NoError(t, err)
	require.Empty(t, inList.Items)

	outList, err = rs.ListSIPOutboundTrunk(ctx, &livekit.ListSIPOutboundTrunkRequest{})
	require.NoError(t, err)
	require.Empty(t, outList.Items)

	old, err = rs.LoadSIPTrunk(ctx, oldID)
	require.Equal(t, service.ErrSIPTrunkNotFound, err)
	require.Nil(t, old)

	in, err = rs.LoadSIPInboundTrunk(ctx, oldID)
	require.Equal(t, service.ErrSIPTrunkNotFound, err)
	require.Nil(t, in)

	out, err = rs.LoadSIPOutboundTrunk(ctx, oldID)
	require.Equal(t, service.ErrSIPTrunkNotFound, err)
	require.Nil(t, out)
}

func TestSIPTrunkList(t *testing.T) {
	s := redisStoreDocker(t)

	testIter(t, func(ctx context.Context, id string) error {
		if strings.HasSuffix(id, "0") {
			return s.StoreSIPTrunk(ctx, &livekit.SIPTrunkInfo{
				SipTrunkId:     id,
				OutboundNumber: id,
			})
		}
		return s.StoreSIPInboundTrunk(ctx, &livekit.SIPInboundTrunkInfo{
			SipTrunkId: id,
			Numbers:    []string{id},
		})
	}, func(ctx context.Context, page *livekit.Pagination, ids []string) iters.PageIter[*livekit.SIPInboundTrunkInfo] {
		return livekit.ListPageIter(s.ListSIPInboundTrunk, &livekit.ListSIPInboundTrunkRequest{
			TrunkIds: ids, Page: page,
		})
	})
}

func TestSIPRuleList(t *testing.T) {
	s := redisStoreDocker(t)

	testIter(t, func(ctx context.Context, id string) error {
		return s.StoreSIPDispatchRule(ctx, &livekit.SIPDispatchRuleInfo{
			SipDispatchRuleId: id,
			TrunkIds:          []string{id},
		})
	}, func(ctx context.Context, page *livekit.Pagination, ids []string) iters.PageIter[*livekit.SIPDispatchRuleInfo] {
		return livekit.ListPageIter(s.ListSIPDispatchRule, &livekit.ListSIPDispatchRuleRequest{
			DispatchRuleIds: ids, Page: page,
		})
	})
}

type listItem interface {
	ID() string
}

func allIDs[T listItem](t testing.TB, it iters.PageIter[T]) []string {
	defer it.Close()
	got, err := iters.AllPages(context.Background(), iters.MapPage(it, func(ctx context.Context, v T) (string, error) {
		return v.ID(), nil
	}))
	require.NoError(t, err)
	return got
}

func testIter[T listItem](
	t *testing.T,
	create func(ctx context.Context, id string) error,
	list func(ctx context.Context, page *livekit.Pagination, ids []string) iters.PageIter[T],
) {
	ctx := context.Background()
	var all []string
	for i := range 250 {
		id := fmt.Sprintf("%05d", i)
		all = append(all, id)
		err := create(ctx, id)
		require.NoError(t, err)
	}

	// List everything with pagination disabled (legacy)
	it := list(ctx, nil, nil)
	got := allIDs(t, it)
	require.Equal(t, all, got)

	// List with pagination enabled
	it = list(ctx, &livekit.Pagination{Limit: 10}, nil)
	got = allIDs(t, it)
	require.Equal(t, all, got)

	// List with pagination enabled, custom ID
	it = list(ctx, &livekit.Pagination{Limit: 10, AfterId: all[55]}, nil)
	got = allIDs(t, it)
	require.Equal(t, all[56:], got)

	// List fixed IDs
	it = list(ctx, &livekit.Pagination{Limit: 10, AfterId: all[5]}, []string{
		all[10],
		all[3],
		"invalid",
		all[8],
	})
	got = allIDs(t, it)
	require.Equal(t, []string{
		all[8],
		all[10],
	}, got)
}
