package main

import (
	"context"
	"crypto/ecdsa"
	"errors"
	"testing"

	"github.com/nspcc-dev/neofs-api-go/v2/object"
	objectGRPC "github.com/nspcc-dev/neofs-api-go/v2/object/grpc"
	"github.com/nspcc-dev/neofs-api-go/v2/refs"
	"github.com/nspcc-dev/neofs-api-go/v2/session"
	"github.com/nspcc-dev/neofs-api-go/v2/signature"
	"github.com/nspcc-dev/neofs-crypto/test"
	"github.com/stretchr/testify/require"
	"google.golang.org/grpc"
)

type testGRPCClient struct {
	server objectGRPC.ObjectServiceServer
}

func (s *testGRPCClient) Get(ctx context.Context, in *objectGRPC.GetRequest, opts ...grpc.CallOption) (objectGRPC.ObjectService_GetClient, error) {
	panic("implement me")
}

func (s *testGRPCClient) Put(ctx context.Context, opts ...grpc.CallOption) (objectGRPC.ObjectService_PutClient, error) {
	panic("implement me")
}

func (s *testGRPCClient) Delete(ctx context.Context, in *objectGRPC.DeleteRequest, opts ...grpc.CallOption) (*objectGRPC.DeleteResponse, error) {
	return s.server.Delete(ctx, in)
}

func (s *testGRPCClient) Head(ctx context.Context, in *objectGRPC.HeadRequest, opts ...grpc.CallOption) (*objectGRPC.HeadResponse, error) {
	return s.server.Head(ctx, in)
}

func (s *testGRPCClient) Search(ctx context.Context, in *objectGRPC.SearchRequest, opts ...grpc.CallOption) (objectGRPC.ObjectService_SearchClient, error) {
	panic("implement me")
}

func (s *testGRPCClient) GetRange(ctx context.Context, in *objectGRPC.GetRangeRequest, opts ...grpc.CallOption) (objectGRPC.ObjectService_GetRangeClient, error) {
	panic("implement me")
}

func (s *testGRPCClient) GetRangeHash(ctx context.Context, in *objectGRPC.GetRangeHashRequest, opts ...grpc.CallOption) (*objectGRPC.GetRangeHashResponse, error) {
	return s.server.GetRangeHash(ctx, in)
}

type testGRPCServer struct {
	key              *ecdsa.PrivateKey
	headResp         *object.HeadResponse
	delResp          *object.DeleteResponse
	getRangeHashResp *object.GetRangeHashResponse
	err              error
}

func (s *testGRPCServer) Get(request *objectGRPC.GetRequest, server objectGRPC.ObjectService_GetServer) error {
	panic("implement me")
}

func (s *testGRPCServer) Put(server objectGRPC.ObjectService_PutServer) error {
	panic("implement me")
}

func (s *testGRPCServer) Delete(ctx context.Context, request *objectGRPC.DeleteRequest) (*objectGRPC.DeleteResponse, error) {
	if s.err != nil {
		return nil, s.err
	}

	// verify request structure
	if err := signature.VerifyServiceMessage(
		object.DeleteRequestFromGRPCMessage(request),
	); err != nil {
		return nil, err
	}

	// sign response structure
	if err := signature.SignServiceMessage(s.key, s.delResp); err != nil {
		return nil, err
	}

	return object.DeleteResponseToGRPCMessage(s.delResp), nil
}

func (s *testGRPCServer) Head(ctx context.Context, request *objectGRPC.HeadRequest) (*objectGRPC.HeadResponse, error) {
	if s.err != nil {
		return nil, s.err
	}

	// verify request structure
	if err := signature.VerifyServiceMessage(
		object.HeadRequestFromGRPCMessage(request),
	); err != nil {
		return nil, err
	}

	// sign response structure
	if err := signature.SignServiceMessage(s.key, s.headResp); err != nil {
		return nil, err
	}

	return object.HeadResponseToGRPCMessage(s.headResp), nil
}

func (s *testGRPCServer) Search(request *objectGRPC.SearchRequest, server objectGRPC.ObjectService_SearchServer) error {
	panic("implement me")
}

func (s *testGRPCServer) GetRange(request *objectGRPC.GetRangeRequest, server objectGRPC.ObjectService_GetRangeServer) error {
	panic("implement me")
}

func (s *testGRPCServer) GetRangeHash(ctx context.Context, request *objectGRPC.GetRangeHashRequest) (*objectGRPC.GetRangeHashResponse, error) {
	if s.err != nil {
		return nil, s.err
	}

	// verify request structure
	if err := signature.VerifyServiceMessage(
		object.GetRangeHashRequestFromGRPCMessage(request),
	); err != nil {
		return nil, err
	}

	// sign response structure
	if err := signature.SignServiceMessage(s.key, s.getRangeHashResp); err != nil {
		return nil, err
	}

	return object.GetRangeHashResponseToGRPCMessage(s.getRangeHashResp), nil
}

func testHeadRequest() *object.HeadRequest {
	cid := new(refs.ContainerID)
	cid.SetValue([]byte{1, 2, 3})

	oid := new(refs.ObjectID)
	oid.SetValue([]byte{4, 5, 6})

	addr := new(refs.Address)
	addr.SetContainerID(cid)
	addr.SetObjectID(oid)

	body := new(object.HeadRequestBody)
	body.SetAddress(addr)

	meta := new(session.RequestMetaHeader)
	meta.SetTTL(1)
	meta.SetXHeaders([]*session.XHeader{})

	req := new(object.HeadRequest)
	req.SetBody(body)
	req.SetMetaHeader(meta)

	return req
}

func testHeadResponse() *object.HeadResponse {
	shortHdr := new(object.ShortHeader)
	shortHdr.SetCreationEpoch(100)

	hdrPart := new(object.GetHeaderPartShort)
	hdrPart.SetShortHeader(shortHdr)

	body := new(object.HeadResponseBody)
	body.SetHeaderPart(hdrPart)

	meta := new(session.ResponseMetaHeader)
	meta.SetTTL(1)
	meta.SetXHeaders([]*session.XHeader{})

	resp := new(object.HeadResponse)
	resp.SetBody(body)
	resp.SetMetaHeader(meta)

	return resp
}

func testDeleteRequest() *object.DeleteRequest {
	cid := new(refs.ContainerID)
	cid.SetValue([]byte{1, 2, 3})

	oid := new(refs.ObjectID)
	oid.SetValue([]byte{4, 5, 6})

	addr := new(refs.Address)
	addr.SetContainerID(cid)
	addr.SetObjectID(oid)

	body := new(object.DeleteRequestBody)
	body.SetAddress(addr)

	meta := new(session.RequestMetaHeader)
	meta.SetTTL(1)
	meta.SetXHeaders([]*session.XHeader{})

	req := new(object.DeleteRequest)
	req.SetBody(body)
	req.SetMetaHeader(meta)

	return req
}

func testDeleteResponse() *object.DeleteResponse {
	body := new(object.DeleteResponseBody)

	meta := new(session.ResponseMetaHeader)
	meta.SetTTL(1)
	meta.SetXHeaders([]*session.XHeader{})

	resp := new(object.DeleteResponse)
	resp.SetBody(body)
	resp.SetMetaHeader(meta)

	return resp
}

func testGetRangeHashRequest() *object.GetRangeHashRequest {
	cid := new(refs.ContainerID)
	cid.SetValue([]byte{1, 2, 3})

	oid := new(refs.ObjectID)
	oid.SetValue([]byte{4, 5, 6})

	addr := new(refs.Address)
	addr.SetContainerID(cid)
	addr.SetObjectID(oid)

	body := new(object.GetRangeHashRequestBody)
	body.SetAddress(addr)

	meta := new(session.RequestMetaHeader)
	meta.SetTTL(1)
	meta.SetXHeaders([]*session.XHeader{})

	req := new(object.GetRangeHashRequest)
	req.SetBody(body)
	req.SetMetaHeader(meta)

	return req
}

func testGetRangeHashResponse() *object.GetRangeHashResponse {
	body := new(object.GetRangeHashResponseBody)
	body.SetHashList([][]byte{{7, 8, 9}})

	meta := new(session.ResponseMetaHeader)
	meta.SetTTL(1)
	meta.SetXHeaders([]*session.XHeader{})

	resp := new(object.GetRangeHashResponse)
	resp.SetBody(body)
	resp.SetMetaHeader(meta)

	return resp
}

func TestGRPCClient_Head(t *testing.T) {
	ctx := context.TODO()

	cliKey := test.DecodeKey(0)
	srvKey := test.DecodeKey(1)

	t.Run("gRPC server error", func(t *testing.T) {
		srvErr := errors.New("test server error")

		srv := &testGRPCServer{
			err: srvErr,
		}

		cli := &testGRPCClient{
			server: srv,
		}

		c, err := object.NewClient(object.WithGRPCServiceClient(cli))
		require.NoError(t, err)

		resp, err := c.Head(ctx, new(object.HeadRequest))
		require.True(t, errors.Is(err, srvErr))
		require.Nil(t, resp)
	})

	t.Run("invalid request structure", func(t *testing.T) {
		req := testHeadRequest()

		require.Error(t, signature.VerifyServiceMessage(req))

		c, err := object.NewClient(
			object.WithGRPCServiceClient(
				&testGRPCClient{
					server: new(testGRPCServer),
				},
			),
		)
		require.NoError(t, err)

		resp, err := c.Head(ctx, req)
		require.Error(t, err)
		require.Nil(t, resp)
	})

	t.Run("correct response", func(t *testing.T) {
		req := testHeadRequest()

		require.NoError(t, signature.SignServiceMessage(cliKey, req))

		resp := testHeadResponse()

		c, err := object.NewClient(
			object.WithGRPCServiceClient(
				&testGRPCClient{
					server: &testGRPCServer{
						key:      srvKey,
						headResp: resp,
					},
				},
			),
		)
		require.NoError(t, err)

		r, err := c.Head(ctx, req)
		require.NoError(t, err)

		require.NoError(t, signature.VerifyServiceMessage(r))
		require.Equal(t, resp.GetBody(), r.GetBody())
		require.Equal(t, resp.GetMetaHeader(), r.GetMetaHeader())
	})
}

func TestGRPCClient_Delete(t *testing.T) {
	ctx := context.TODO()

	cliKey := test.DecodeKey(0)
	srvKey := test.DecodeKey(1)

	t.Run("gRPC server error", func(t *testing.T) {
		srvErr := errors.New("test server error")

		srv := &testGRPCServer{
			err: srvErr,
		}

		cli := &testGRPCClient{
			server: srv,
		}

		c, err := object.NewClient(object.WithGRPCServiceClient(cli))
		require.NoError(t, err)

		resp, err := c.Delete(ctx, new(object.DeleteRequest))
		require.True(t, errors.Is(err, srvErr))
		require.Nil(t, resp)
	})

	t.Run("invalid request structure", func(t *testing.T) {
		req := testDeleteRequest()

		require.Error(t, signature.VerifyServiceMessage(req))

		c, err := object.NewClient(
			object.WithGRPCServiceClient(
				&testGRPCClient{
					server: new(testGRPCServer),
				},
			),
		)
		require.NoError(t, err)

		resp, err := c.Delete(ctx, req)
		require.Error(t, err)
		require.Nil(t, resp)
	})

	t.Run("correct response", func(t *testing.T) {
		req := testDeleteRequest()

		require.NoError(t, signature.SignServiceMessage(cliKey, req))

		resp := testDeleteResponse()

		c, err := object.NewClient(
			object.WithGRPCServiceClient(
				&testGRPCClient{
					server: &testGRPCServer{
						key:     srvKey,
						delResp: resp,
					},
				},
			),
		)
		require.NoError(t, err)

		r, err := c.Delete(ctx, req)
		require.NoError(t, err)

		require.NoError(t, signature.VerifyServiceMessage(r))
		require.Equal(t, resp.GetBody(), r.GetBody())
		require.Equal(t, resp.GetMetaHeader(), r.GetMetaHeader())
	})
}

func TestGRPCClient_GetRangeHash(t *testing.T) {
	ctx := context.TODO()

	cliKey := test.DecodeKey(0)
	srvKey := test.DecodeKey(1)

	t.Run("gRPC server error", func(t *testing.T) {
		srvErr := errors.New("test server error")

		srv := &testGRPCServer{
			err: srvErr,
		}

		cli := &testGRPCClient{
			server: srv,
		}

		c, err := object.NewClient(object.WithGRPCServiceClient(cli))
		require.NoError(t, err)

		resp, err := c.GetRangeHash(ctx, new(object.GetRangeHashRequest))
		require.True(t, errors.Is(err, srvErr))
		require.Nil(t, resp)
	})

	t.Run("invalid request structure", func(t *testing.T) {
		req := testGetRangeHashRequest()

		require.Error(t, signature.VerifyServiceMessage(req))

		c, err := object.NewClient(
			object.WithGRPCServiceClient(
				&testGRPCClient{
					server: new(testGRPCServer),
				},
			),
		)
		require.NoError(t, err)

		resp, err := c.GetRangeHash(ctx, req)
		require.Error(t, err)
		require.Nil(t, resp)
	})

	t.Run("correct response", func(t *testing.T) {
		req := testGetRangeHashRequest()

		require.NoError(t, signature.SignServiceMessage(cliKey, req))

		resp := testGetRangeHashResponse()

		c, err := object.NewClient(
			object.WithGRPCServiceClient(
				&testGRPCClient{
					server: &testGRPCServer{
						key:              srvKey,
						getRangeHashResp: resp,
					},
				},
			),
		)
		require.NoError(t, err)

		r, err := c.GetRangeHash(ctx, req)
		require.NoError(t, err)

		require.NoError(t, signature.VerifyServiceMessage(r))
		require.Equal(t, resp.GetBody(), r.GetBody())
		require.Equal(t, resp.GetMetaHeader(), r.GetMetaHeader())
	})
}