// Copyright 2021 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package camera

import (
	"context"
	"net/http"
	"net/http/httptest"
	"path/filepath"
	"time"

	"github.com/golang/protobuf/ptypes/empty"
	"google.golang.org/grpc"

	"chromiumos/tast/ctxutil"
	"chromiumos/tast/errors"
	"chromiumos/tast/fsutil"
	"chromiumos/tast/local/chrome"
	"chromiumos/tast/local/chrome/uiauto/crd"
	"chromiumos/tast/local/cryptohome"
	pb "chromiumos/tast/services/cros/camerabox"
	"chromiumos/tast/testing"
)

func init() {
	testing.AddService(&testing.Service{
		Register: func(srv *grpc.Server, s *testing.ServiceState) {
			pb.RegisterAlignmentServiceServer(srv, &AlignmentService{s: s})
		},
	})
}

type AlignmentService struct {
	s *testing.ServiceState
}

func (a *AlignmentService) ManualAlign(ctx context.Context, req *pb.ManualAlignRequest) (*empty.Empty, error) {
	srv := httptest.NewServer(http.FileServer(http.Dir(req.DataPath)))
	defer srv.Close()

	cr, err := chrome.New(ctx, chrome.ARCDisabled(),
		chrome.GAIALogin(chrome.Creds{User: req.Username, Pass: req.Password}),
		chrome.KeepState(),
		// Avoid the need to grant camera/microphone permissions.
		chrome.ExtraArgs("--use-fake-ui-for-media-stream"))
	if err != nil {
		return nil, errors.Wrap(err, "failed to start chrome")
	}
	defer cr.Close(ctx)

	tconn, err := cr.TestAPIConn(ctx)
	if err != nil {
		return nil, errors.Wrap(err, "failed to connect Test API")
	}
	// TODO(b/166370953): Handle CRD timeout.
	if err := crd.Launch(ctx, cr, tconn); err != nil {
		return nil, errors.Wrap(err, "failed to launch remote desktop")
	}
	testing.ContextLog(ctx, "Waiting connection")
	if err := crd.WaitConnection(ctx, tconn); err != nil {
		return nil, errors.Wrap(err, "no client connected")
	}

	conn, err := cr.NewConn(ctx, srv.URL+"/camerabox_align.html")
	if err != nil {
		return nil, errors.Wrap(err, "failed to open camerabox_align.html")
	}
	defer func() {
		conn.CloseTarget(ctx)
		conn.Close()
	}()

	if err := conn.Call(ctx, nil, "Tast.manualAlign", req.Facing); err != nil {
		return nil, errors.Wrap(err, "failed to call manualAlign on camerabox_align.html")
	}

	return &empty.Empty{}, nil
}

// copyPreviewFrame copies the last preview frame image from download folder for debugging.
func (a *AlignmentService) copyPreviewFrame(ctx context.Context, cr *chrome.Chrome) error {
	userPath, err := cryptohome.UserPath(ctx, cr.NormalizedUser())
	if err != nil {
		return errors.Wrap(err, "failed to get the cryptohome user path")
	}
	const frameFileName = "frame.png"
	framePath := filepath.Join(userPath, "MyFiles", "Downloads", frameFileName)
	outDir, ok := testing.ContextOutDir(ctx)
	if !ok {
		return errors.New("failed to get remote context dir")
	}
	if err := fsutil.CopyFile(framePath, filepath.Join(outDir, frameFileName)); err != nil {
		return errors.Wrap(err, "failed copy frame image to out dir")
	}
	return nil
}

func (a *AlignmentService) CheckRegression(ctx context.Context, req *pb.CheckRegressionRequest) (*pb.CheckRegressionResponse, error) {
	srv := httptest.NewServer(http.FileServer(http.Dir(req.DataPath)))
	defer srv.Close()

	cleanupCtx := ctx
	ctx, cancel := ctxutil.Shorten(ctx, 10*time.Second)
	defer cancel()

	cr, err := chrome.New(ctx, chrome.ARCDisabled(),
		// Avoid the need to grant camera/microphone permissions.
		chrome.ExtraArgs("--use-fake-ui-for-media-stream"))
	if err != nil {
		return nil, errors.Wrap(err, "failed to start chrome")
	}
	defer cr.Close(cleanupCtx)

	conn, err := cr.NewConn(ctx, srv.URL+"/camerabox_align.html")
	if err != nil {
		return nil, errors.Wrap(err, "failed to open camerabox_align.html")
	}
	defer conn.Close()
	defer conn.CloseTarget(cleanupCtx)

	if err := conn.Call(ctx, nil, "Tast.checkRegression", req.Facing); err != nil {
		if err := a.copyPreviewFrame(cleanupCtx, cr); err != nil {
			testing.ContextLog(ctx, "Failed to copy last preview frame: ", err)
		}
		return &pb.CheckRegressionResponse{
			Result: pb.TestResult_TEST_RESULT_FAILED,
			Error:  err.Error(),
		}, nil
	}

	return &pb.CheckRegressionResponse{Result: pb.TestResult_TEST_RESULT_PASSED}, nil
}
