package taskruns

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"strings"
	"testing"

	"github.com/pingcap/test-infra/common/client/util"
	commM "github.com/pingcap/test-infra/common/model"
	"github.com/pingcap/test-infra/common/model/tcms/taskruns"
	"github.com/stretchr/testify/require"
)

type fakeTcmsClient struct {
	handler httpHandler
}

type httpHandler func(req *http.Request) (*http.Response, error)

func (f httpHandler) RoundTrip(req *http.Request) (*http.Response, error) {
	return f(req)
}

func (f fakeTcmsClient) NewRequest() *util.Request {
	return util.NewRequest().Client(&http.Client{Transport: f.handler}).Url("https://tcms.com")
}

func newTestbedSetupClient(f httpHandler) testbedSetupClient {
	return NewTestbedSetupClient(fakeTcmsClient{handler: f}).(testbedSetupClient)
}

func TestTestbedSetupClient_List(t *testing.T) {
	client := newTestbedSetupClient(func(req *http.Request) (*http.Response, error) {
		require.Equal(t, http.MethodGet, req.Method)
		require.Equal(t, "https://tcms.com/api/v1/testbedsetups?limit=0&planExecIDs=1%2C12%2C123&scheduleStatus=pending", req.URL.String())
		return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(`
	{
	  "apiVersion": "api/v1",
	  "kind": "TestbedSetupList"
	}`))}, nil
	})
	status := taskruns.ScheduleStatusPending
	list, err := client.List(context.TODO(), taskruns.ListTestbedSetupOpts{ScheduleStatus: &status, PlanExecIDs: []commM.ID{1,12,123}})
	require.NoError(t, err)
	expected := &taskruns.TestbedSetupList{GeneralHeader: taskruns.HeaderTestbedSetupList}
	require.Equal(t, expected, list)
}

func TestTestbedSetupClient_List_Fail(t *testing.T) {
	client := newTestbedSetupClient(func(req *http.Request) (*http.Response, error) {
		require.Equal(t, http.MethodGet, req.Method)
		require.Equal(t, "https://tcms.com/api/v1/testbedsetups?limit=0&scheduleStatus=pending", req.URL.String())
		return &http.Response{StatusCode: http.StatusBadRequest}, nil
	})
	status := taskruns.ScheduleStatusPending
	list, err := client.List(context.TODO(), taskruns.ListTestbedSetupOpts{ScheduleStatus: &status})
	require.Equal(t, fmt.Errorf("bad http code: 400"), err)
	require.Nil(t, list)
}

func TestTestbedSetupClient_Get(t *testing.T) {
	client := newTestbedSetupClient(func(req *http.Request) (*http.Response, error) {
		require.Equal(t, http.MethodGet, req.Method)
		require.Equal(t, "https://tcms.com/api/v1/testbedsetups/234", req.URL.String())
		return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(`
	{
	  "apiVersion": "api/v1",
	  "kind": "TestbedSetup",
	  "metadata": {
		"id": 234
	  }
	}`))}, nil
	})
	list, err := client.Get(context.TODO(), 234, taskruns.GetTestbedSetupOpts{})
	require.NoError(t, err)
	expected := &taskruns.TestbedSetup{GeneralHeader: taskruns.HeaderTestbedSetup, MetaData: taskruns.GeneralMetaData{ID: 234}}
	require.Equal(t, expected, list)
}

func TestTestbedSetupClient_Get_Fail(t *testing.T) {
	client := newTestbedSetupClient(func(req *http.Request) (*http.Response, error) {
		require.Equal(t, http.MethodGet, req.Method)
		require.Equal(t, "https://tcms.com/api/v1/testbedsetups/234", req.URL.String())
		return nil, fmt.Errorf("timeout")
	})
	actual, err := client.Get(context.TODO(), 234, taskruns.GetTestbedSetupOpts{})
	require.Error(t, fmt.Errorf("timeout"), err)
	require.Nil(t, actual)
}

func TestTestbedSetupClient_PartialUpdate(t *testing.T) {
	for _, s := range []struct {
		Part string
		Body taskruns.TestbedSetup
		Resp string
		URL  string
	}{
		{
			Part: "schedule",
			Body: taskruns.TestbedSetup{
				GeneralHeader: taskruns.HeaderTestbedSetup,
				MetaData:      taskruns.GeneralMetaData{ID: 234},
				Status: taskruns.TestbedSetupStatus{
					Schedule: &taskruns.Schedule{ScheduleStatus: taskruns.ScheduleStatusApproved}},
			},
			Resp: `{
	  "apiVersion": "api/v1",
	  "kind": "TestbedSetup",
	  "metadata": {
		"id": 234
	  }
	}`,
			URL: "https://tcms.com/api/v1/testbedsetups/234/status/schedule",
		},
		{
			Part: "run",
			Body: taskruns.TestbedSetup{
				GeneralHeader: taskruns.HeaderTestbedSetup,
				MetaData:      taskruns.GeneralMetaData{ID: 234},
				Status: taskruns.TestbedSetupStatus{
					TaskRun: &taskruns.TaskRunStatus{Phase: taskruns.PhaseRunning}},
			},
			Resp: `{
	  "apiVersion": "api/v1",
	  "kind": "TestbedSetup",
	  "metadata": {
		"id": 234
	  }
	}`,
			URL: "https://tcms.com/api/v1/testbedsetups/234/status/run",
		},
		{
			Part: "report",
			Body: taskruns.TestbedSetup{
				GeneralHeader: taskruns.HeaderTestbedSetup,
				MetaData:      taskruns.GeneralMetaData{ID: 234},
				Status: taskruns.TestbedSetupStatus{
					Report: &taskruns.TestbedSetupReport{RetryCnt: 4}},
			},
			Resp: `{
	  "apiVersion": "api/v1",
	  "kind": "TestbedSetup",
	  "metadata": {
		"id": 234
	  }
	}`,
			URL: "https://tcms.com/api/v1/testbedsetups/234/status/report",
		},
	} {
		t.Run(s.Part, func(t *testing.T) {
			client := newTestbedSetupClient(func(req *http.Request) (*http.Response, error) {
				require.Equal(t, http.MethodPut, req.Method)
				require.Equal(t, s.URL, req.URL.String())
				expectedBody, err := json.Marshal(s.Body)
				require.NoError(t, err)
				actualBody, err := io.ReadAll(req.Body)
				require.NoError(t, err)
				require.JSONEq(t, string(expectedBody), string(actualBody))
				return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(s.Resp))}, nil
			})
			actual, err := client.updatePartialStatus(context.TODO(), s.Body, taskruns.GeneralUpdateOpts{}, s.Part)
			require.NoError(t, err)
			expected := &taskruns.TestbedSetup{GeneralHeader: taskruns.HeaderTestbedSetup, MetaData: taskruns.GeneralMetaData{ID: 234}}
			require.Equal(t, expected, actual)
		})
	}
}

func TestTestbedSetupClient_UpdateSchedule_Fail(t *testing.T) {
	client := newTestbedSetupClient(func(req *http.Request) (*http.Response, error) {
		require.Equal(t, "https://tcms.com/api/v1/testbedsetups/0/status/schedule", req.URL.String())
		return &http.Response{StatusCode: http.StatusBadRequest}, nil
	})
	actual, err := client.UpdateSchedule(context.TODO(), taskruns.NewTestbedSetup(), taskruns.GeneralUpdateOpts{})
	require.Equal(t, fmt.Errorf("bad http code: 400"), err)
	require.Nil(t, actual)
}

func TestTestbedSetupClient_Create(t *testing.T) {
	body := taskruns.TestbedSetup{
		GeneralHeader: taskruns.HeaderTestbedSetup,
		Status: taskruns.TestbedSetupStatus{
			TaskRun: &taskruns.TaskRunStatus{Phase: taskruns.PhaseRunning}},
	}
	resp := taskruns.TestbedSetup{
		GeneralHeader: taskruns.HeaderTestbedSetup,
		MetaData:      taskruns.GeneralMetaData{ID: 666},
		Status: taskruns.TestbedSetupStatus{
			TaskRun: &taskruns.TaskRunStatus{Phase: taskruns.PhaseRunning}},
	}
	client := newTestbedSetupClient(func(req *http.Request) (*http.Response, error) {
		require.Equal(t, http.MethodPost, req.Method)
		require.Equal(t, "https://tcms.com/api/v1/testbedsetups", req.URL.String())
		expectedBody, err := json.Marshal(body)
		require.NoError(t, err)
		actualBody, err := io.ReadAll(req.Body)
		require.NoError(t, err)
		require.JSONEq(t, string(expectedBody), string(actualBody))
		respBody, err := json.Marshal(resp)
		require.NoError(t, err)
		return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader(respBody))}, nil
	})
	actual, err := client.Create(context.TODO(), body, taskruns.CreateTestbedSetupOpts{})
	require.NoError(t, err)
	require.Equal(t, &resp, actual)
}

func TestTestbedSetupClient_Create_DryRun(t *testing.T) {
	client := newTestbedSetupClient(func(req *http.Request) (*http.Response, error) {
		require.Equal(t, "https://tcms.com/api/v1/testbedsetups?dryRun=true", req.URL.String())
		return &http.Response{StatusCode: http.StatusBadRequest}, nil
	})
	actual, err := client.Create(context.TODO(), taskruns.NewTestbedSetup(), taskruns.CreateTestbedSetupOpts{GeneralCreateOpts: taskruns.GeneralCreateOpts{DryRun: true}})
	require.Equal(t, fmt.Errorf("bad http code: 400"), err)
	require.Nil(t, actual)
}

func TestTestbedSetupClient_Create_Code_Error(t *testing.T) {
	client := newTestbedSetupClient(func(req *http.Request) (*http.Response, error) {
		return &http.Response{StatusCode: http.StatusBadRequest}, nil
	})
	actual, err := client.Create(context.TODO(), taskruns.NewTestbedSetup(), taskruns.CreateTestbedSetupOpts{})
	require.Equal(t, fmt.Errorf("bad http code: 400"), err)
	require.Nil(t, actual)
}

func TestTestbedSetupClient_Create_DataFormat_Error(t *testing.T) {
	client := newTestbedSetupClient(func(req *http.Request) (*http.Response, error) {
		return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader("he-he"))}, nil
	})
	actual, err := client.Create(context.TODO(), taskruns.NewTestbedSetup(), taskruns.CreateTestbedSetupOpts{})
	require.Equal(t, "invalid character 'h' looking for beginning of value", err.Error())
	require.Nil(t, actual)
}

func TestTestbedSetupClient_Create_DataContent_Error(t *testing.T) {
	body, err := json.Marshal(taskruns.NewTestbedSetupList())
	require.NoError(t, err)
	client := newTestbedSetupClient(func(req *http.Request) (*http.Response, error) {
		return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader(body))}, nil
	})
	actual, err := client.Create(context.TODO(), taskruns.NewTestbedSetup(), taskruns.CreateTestbedSetupOpts{})
	require.Equal(t, "json: unknown field \"items\"", err.Error())
	require.Nil(t, actual)
}
