// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0

package auth_test

import (
	"testing"
	"time"

	apiutil "github.com/absmach/magistrala/api/http/util"
	"github.com/absmach/magistrala/auth"
	channelsOps "github.com/absmach/magistrala/channels/operations"
	clientsOps "github.com/absmach/magistrala/clients/operations"
	groupsOps "github.com/absmach/magistrala/groups/operations"
	"github.com/stretchr/testify/assert"
)

func TestScopeAuthorized(t *testing.T) {
	cases := []struct {
		desc       string
		scope      *auth.Scope
		entityType auth.EntityType
		domainID   string
		operation  string
		entityID   string
		expected   bool
	}{
		{
			desc: "Authorized with matching entity type, domain, operation and entity ID",
			scope: &auth.Scope{
				EntityType: auth.GroupsType,
				DomainID:   "domain1",
				Operation:  "view",
				EntityID:   "entity1",
			},
			entityType: auth.GroupsType,
			domainID:   "domain1",
			operation:  "view",
			entityID:   "entity1",
			expected:   true,
		},
		{
			desc: "Authorized with wildcard entity ID",
			scope: &auth.Scope{
				EntityType: auth.GroupsType,
				DomainID:   "domain1",
				Operation:  "view",
				EntityID:   "*",
			},
			entityType: auth.GroupsType,
			domainID:   "domain1",
			operation:  "view",
			entityID:   "any-entity",
			expected:   true,
		},
		{
			desc: "Authorized without domain ID",
			scope: &auth.Scope{
				EntityType: auth.ClientsType,
				DomainID:   "",
				Operation:  "view",
				EntityID:   "client1",
			},
			entityType: auth.ClientsType,
			domainID:   "domain1",
			operation:  "view",
			entityID:   "client1",
			expected:   true,
		},
		{
			desc: "Not authorized with different entity type",
			scope: &auth.Scope{
				EntityType: auth.GroupsType,
				DomainID:   "domain1",
				Operation:  "view",
				EntityID:   "entity1",
			},
			entityType: auth.ChannelsType,
			domainID:   "domain1",
			operation:  "view",
			entityID:   "entity1",
			expected:   false,
		},
		{
			desc: "Not authorized with different domain ID",
			scope: &auth.Scope{
				EntityType: auth.GroupsType,
				DomainID:   "domain1",
				Operation:  "view",
				EntityID:   "entity1",
			},
			entityType: auth.GroupsType,
			domainID:   "domain2",
			operation:  "view",
			entityID:   "entity1",
			expected:   false,
		},
		{
			desc: "Not authorized with different operation",
			scope: &auth.Scope{
				EntityType: auth.GroupsType,
				DomainID:   "domain1",
				Operation:  "view",
				EntityID:   "entity1",
			},
			entityType: auth.GroupsType,
			domainID:   "domain1",
			operation:  "delete",
			entityID:   "entity1",
			expected:   false,
		},
		{
			desc: "Not authorized with different entity ID",
			scope: &auth.Scope{
				EntityType: auth.GroupsType,
				DomainID:   "domain1",
				Operation:  "view",
				EntityID:   "entity1",
			},
			entityType: auth.GroupsType,
			domainID:   "domain1",
			operation:  "view",
			entityID:   "entity2",
			expected:   false,
		},
		{
			desc:       "Not authorized with nil scope",
			scope:      nil,
			entityType: auth.GroupsType,
			domainID:   "domain1",
			operation:  "view",
			entityID:   "entity1",
			expected:   false,
		},
	}

	for _, tc := range cases {
		t.Run(tc.desc, func(t *testing.T) {
			result := tc.scope.Authorized(tc.entityType, tc.domainID, tc.operation, tc.entityID)
			assert.Equal(t, tc.expected, result, "Authorized() = %v, expected %v", result, tc.expected)
		})
	}
}

func TestScopeValidate(t *testing.T) {
	cases := []struct {
		desc  string
		scope *auth.Scope
		err   error
	}{
		{
			desc: "Valid scope for groups with domain ID",
			scope: &auth.Scope{
				EntityType: auth.GroupsType,
				DomainID:   "domain1",
				Operation:  "view",
				EntityID:   "entity1",
			},
			err: nil,
		},
		{
			desc: "Valid scope for channels with domain ID",
			scope: &auth.Scope{
				EntityType: auth.ChannelsType,
				DomainID:   "domain1",
				Operation:  "view",
				EntityID:   "channel1",
			},
			err: nil,
		},
		{
			desc: "Valid scope for clients with domain ID",
			scope: &auth.Scope{
				EntityType: auth.ClientsType,
				DomainID:   "domain1",
				Operation:  "update",
				EntityID:   "client1",
			},
			err: nil,
		},
		{
			desc: "Valid scope for messages with domain ID",
			scope: &auth.Scope{
				EntityType: auth.MessagesType,
				DomainID:   "domain1",
				Operation:  "message_publish",
				EntityID:   "message1",
			},
			err: nil,
		},
		{
			desc: "Valid scope for dashboard with domain ID",
			scope: &auth.Scope{
				EntityType: auth.DashboardType,
				DomainID:   "domain1",
				Operation:  "dashboard_share",
				EntityID:   "dashboard1",
			},
			err: nil,
		},
		{
			desc: "Valid scope with wildcard entity ID",
			scope: &auth.Scope{
				EntityType: auth.GroupsType,
				DomainID:   "domain1",
				Operation:  "view",
				EntityID:   "*",
			},
			err: nil,
		},
		{
			desc:  "Invalid nil scope",
			scope: nil,
			err:   assert.AnError, // Will be checked with Contains
		},
		{
			desc: "Invalid scope without entity ID",
			scope: &auth.Scope{
				EntityType: auth.GroupsType,
				DomainID:   "domain1",
				Operation:  groupsOps.OperationDetails()[groupsOps.OpViewGroup].Name,
				EntityID:   "",
			},
			err: apiutil.ErrMissingEntityID,
		},
		{
			desc: "Invalid scope for groups without domain ID",
			scope: &auth.Scope{
				EntityType: auth.GroupsType,
				DomainID:   "",
				Operation:  groupsOps.OperationDetails()[groupsOps.OpViewGroup].Name,
				EntityID:   "entity1",
			},
			err: apiutil.ErrMissingDomainID,
		},
		{
			desc: "Invalid scope for channels without domain ID",
			scope: &auth.Scope{
				EntityType: auth.ChannelsType,
				DomainID:   "",
				Operation:  channelsOps.OperationDetails()[channelsOps.OpViewChannel].Name,
				EntityID:   "channel1",
			},
			err: apiutil.ErrMissingDomainID,
		},
		{
			desc: "Invalid scope for clients without domain ID",
			scope: &auth.Scope{
				EntityType: auth.ClientsType,
				DomainID:   "",
				Operation:  clientsOps.OperationDetails()[clientsOps.OpViewClient].Name,
				EntityID:   "client1",
			},
			err: apiutil.ErrMissingDomainID,
		},
		{
			desc: "Invalid scope for dashboard without domain ID",
			scope: &auth.Scope{
				EntityType: auth.DashboardType,
				DomainID:   "",
				Operation:  auth.OpShare,
				EntityID:   "dashboard1",
			},
			err: apiutil.ErrMissingDomainID,
		},
		{
			desc: "Invalid scope for messages without domain ID",
			scope: &auth.Scope{
				EntityType: auth.MessagesType,
				DomainID:   "",
				Operation:  auth.OpPublish,
				EntityID:   "message1",
			},
			err: apiutil.ErrMissingDomainID,
		},
	}

	for _, tc := range cases {
		t.Run(tc.desc, func(t *testing.T) {
			err := tc.scope.Validate()
			if tc.err != nil {
				assert.Error(t, err, "Validate() should return error")
				if tc.err != assert.AnError {
					assert.Equal(t, tc.err, err, "Validate() error = %v, expected %v", err, tc.err)
				}
			} else {
				assert.NoError(t, err, "Validate() should not return error")
			}
		})
	}
}

func TestPATValidate(t *testing.T) {
	cases := []struct {
		desc string
		pat  *auth.PAT
		err  bool
	}{
		{
			desc: "Valid PAT",
			pat: &auth.PAT{
				ID:          "pat-id",
				User:        "user-id",
				Name:        "test-pat",
				Description: "test description",
			},
			err: false,
		},
		{
			desc: "Invalid nil PAT",
			pat:  nil,
			err:  true,
		},
		{
			desc: "Invalid PAT without name",
			pat: &auth.PAT{
				ID:          "pat-id",
				User:        "user-id",
				Name:        "",
				Description: "test description",
			},
			err: true,
		},
		{
			desc: "Invalid PAT without user",
			pat: &auth.PAT{
				ID:          "pat-id",
				User:        "",
				Name:        "test-pat",
				Description: "test description",
			},
			err: true,
		},
	}

	for _, tc := range cases {
		t.Run(tc.desc, func(t *testing.T) {
			err := tc.pat.Validate()
			if tc.err {
				assert.Error(t, err, "Validate() should return error")
			} else {
				assert.NoError(t, err, "Validate() should not return error")
			}
		})
	}
}

func TestPATMarshalUnmarshalBinary(t *testing.T) {
	pat := auth.PAT{
		ID:          "pat-id",
		User:        "user-id",
		Name:        "test-pat",
		Description: "test description",
		Secret:      "secret",
		IssuedAt:    time.Now().UTC().Round(time.Second),
		ExpiresAt:   time.Now().UTC().Add(24 * time.Hour).Round(time.Second),
		Status:      auth.ActiveStatus,
	}

	// Marshal
	data, err := pat.MarshalBinary()
	assert.NoError(t, err, "MarshalBinary() should not return error")
	assert.NotNil(t, data, "MarshalBinary() should return data")

	// Unmarshal
	var newPAT auth.PAT
	err = newPAT.UnmarshalBinary(data)
	assert.NoError(t, err, "UnmarshalBinary() should not return error")

	assert.Equal(t, pat.ID, newPAT.ID, "ID mismatch")
	assert.Equal(t, pat.User, newPAT.User, "User mismatch")
	assert.Equal(t, pat.Name, newPAT.Name, "Name mismatch")
	assert.Equal(t, pat.Description, newPAT.Description, "Description mismatch")
	assert.Equal(t, pat.Secret, newPAT.Secret, "Secret mismatch")
	assert.Equal(t, pat.Status, newPAT.Status, "Status mismatch")
}
