package onedrive

import (
	"context"
	"database/sql"
	"errors"
	"github.com/cloudreve/Cloudreve/v3/pkg/mocks/controllermock"
	"io"
	"io/ioutil"
	"net/http"
	"net/url"
	"strings"
	"testing"
	"time"

	"github.com/DATA-DOG/go-sqlmock"
	model "github.com/cloudreve/Cloudreve/v3/models"
	"github.com/cloudreve/Cloudreve/v3/pkg/cache"
	"github.com/cloudreve/Cloudreve/v3/pkg/request"
	"github.com/jinzhu/gorm"
	"github.com/stretchr/testify/assert"
	testMock "github.com/stretchr/testify/mock"
)

var mock sqlmock.Sqlmock

// TestMain 初始化数据库Mock
func TestMain(m *testing.M) {
	var db *sql.DB
	var err error
	db, mock, err = sqlmock.New()
	if err != nil {
		panic("An error was not expected when opening a stub database connection")
	}
	model.DB, _ = gorm.Open("mysql", db)
	defer db.Close()
	m.Run()
}

func TestGetOAuthEndpoint(t *testing.T) {
	asserts := assert.New(t)

	// URL解析失败
	{
		client := Client{
			Endpoints: &Endpoints{
				OAuthURL: string([]byte{0x7f}),
			},
		}
		res := client.getOAuthEndpoint()
		asserts.Nil(res)
	}

	{
		testCase := []struct {
			OAuthURL string
			token    string
			auth     string
			isChina  bool
		}{
			{
				OAuthURL: "http://login.live.com",
				token:    "https://login.live.com/oauth20_token.srf",
				auth:     "https://login.live.com/oauth20_authorize.srf",
				isChina:  false,
			},
			{
				OAuthURL: "http://login.chinacloudapi.cn",
				token:    "https://login.chinacloudapi.cn/common/oauth2/v2.0/token",
				auth:     "https://login.chinacloudapi.cn/common/oauth2/v2.0/authorize",
				isChina:  true,
			},
			{
				OAuthURL: "other",
				token:    "https://login.microsoftonline.com/common/oauth2/v2.0/token",
				auth:     "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
				isChina:  false,
			},
		}

		for i, testCase := range testCase {
			client := Client{
				Endpoints: &Endpoints{
					OAuthURL: testCase.OAuthURL,
				},
			}
			res := client.getOAuthEndpoint()
			asserts.Equal(testCase.token, res.token.String(), "Test Case #%d", i)
			asserts.Equal(testCase.auth, res.authorize.String(), "Test Case #%d", i)
			asserts.Equal(testCase.isChina, client.Endpoints.isInChina, "Test Case #%d", i)
		}
	}
}

func TestClient_OAuthURL(t *testing.T) {
	asserts := assert.New(t)

	client := Client{
		ClientID:  "client_id",
		Redirect:  "http://cloudreve.org/callback",
		Endpoints: &Endpoints{},
	}
	client.Endpoints.OAuthEndpoints = client.getOAuthEndpoint()
	res, err := url.Parse(client.OAuthURL(context.Background(), []string{"scope1", "scope2"}))
	asserts.NoError(err)
	query := res.Query()
	asserts.Equal("client_id", query.Get("client_id"))
	asserts.Equal("scope1 scope2", query.Get("scope"))
	asserts.Equal(client.Redirect, query.Get("redirect_uri"))

}

type ClientMock struct {
	testMock.Mock
}

func (m ClientMock) Request(method, target string, body io.Reader, opts ...request.Option) *request.Response {
	args := m.Called(method, target, body, opts)
	return args.Get(0).(*request.Response)
}

type mockReader string

func (r mockReader) Read(b []byte) (int, error) {
	return 0, errors.New("read error")
}

func TestClient_ObtainToken(t *testing.T) {
	asserts := assert.New(t)

	client := Client{
		Endpoints:    &Endpoints{},
		ClientID:     "ClientID",
		ClientSecret: "ClientSecret",
		Redirect:     "Redirect",
	}
	client.Endpoints.OAuthEndpoints = client.getOAuthEndpoint()

	// 刷新Token 成功
	{
		clientMock := ClientMock{}
		clientMock.On(
			"Request",
			"POST",
			client.Endpoints.OAuthEndpoints.token.String(),
			testMock.Anything,
			testMock.Anything,
		).Return(&request.Response{
			Err: nil,
			Response: &http.Response{
				StatusCode: 200,
				Body:       ioutil.NopCloser(strings.NewReader(`{"access_token":"i am token"}`)),
			},
		})
		client.Request = clientMock

		res, err := client.ObtainToken(context.Background())
		clientMock.AssertExpectations(t)
		asserts.NoError(err)
		asserts.NotNil(res)
		asserts.Equal("i am token", res.AccessToken)
	}

	// 重新获取 无法发送请求
	{
		clientMock := ClientMock{}
		clientMock.On(
			"Request",
			"POST",
			client.Endpoints.OAuthEndpoints.token.String(),
			testMock.Anything,
			testMock.Anything,
		).Return(&request.Response{
			Err: errors.New("error"),
		})
		client.Request = clientMock

		res, err := client.ObtainToken(context.Background(), WithCode("code"))
		clientMock.AssertExpectations(t)
		asserts.Error(err)
		asserts.Nil(res)
	}

	// 刷新Token 无法获取响应正文
	{
		clientMock := ClientMock{}
		clientMock.On(
			"Request",
			"POST",
			client.Endpoints.OAuthEndpoints.token.String(),
			testMock.Anything,
			testMock.Anything,
		).Return(&request.Response{
			Err: nil,
			Response: &http.Response{
				StatusCode: 200,
				Body:       ioutil.NopCloser(mockReader("")),
			},
		})
		client.Request = clientMock

		res, err := client.ObtainToken(context.Background())
		clientMock.AssertExpectations(t)
		asserts.Error(err)
		asserts.Nil(res)
		asserts.Equal("read error", err.Error())
	}

	// 刷新Token OneDrive返回错误
	{
		clientMock := ClientMock{}
		clientMock.On(
			"Request",
			"POST",
			client.Endpoints.OAuthEndpoints.token.String(),
			testMock.Anything,
			testMock.Anything,
		).Return(&request.Response{
			Err: nil,
			Response: &http.Response{
				StatusCode: 400,
				Body:       ioutil.NopCloser(strings.NewReader(`{"error":"i am error"}`)),
			},
		})
		client.Request = clientMock

		res, err := client.ObtainToken(context.Background())
		clientMock.AssertExpectations(t)
		asserts.Error(err)
		asserts.Nil(res)
		asserts.Equal("", err.Error())
	}

	// 刷新Token OneDrive未知响应
	{
		clientMock := ClientMock{}
		clientMock.On(
			"Request",
			"POST",
			client.Endpoints.OAuthEndpoints.token.String(),
			testMock.Anything,
			testMock.Anything,
		).Return(&request.Response{
			Err: nil,
			Response: &http.Response{
				StatusCode: 400,
				Body:       ioutil.NopCloser(strings.NewReader(`？？？`)),
			},
		})
		client.Request = clientMock

		res, err := client.ObtainToken(context.Background())
		clientMock.AssertExpectations(t)
		asserts.Error(err)
		asserts.Nil(res)
	}
}

func TestClient_UpdateCredential(t *testing.T) {
	asserts := assert.New(t)
	client := Client{
		Policy:       &model.Policy{Model: gorm.Model{ID: 257}},
		Endpoints:    &Endpoints{},
		ClientID:     "TestClient_UpdateCredential",
		ClientSecret: "ClientSecret",
		Redirect:     "Redirect",
		Credential:   &Credential{},
	}
	client.Endpoints.OAuthEndpoints = client.getOAuthEndpoint()

	// 无有效的RefreshToken
	{
		err := client.UpdateCredential(context.Background(), false)
		asserts.Equal(ErrInvalidRefreshToken, err)
		client.Credential = nil
		err = client.UpdateCredential(context.Background(), false)
		asserts.Equal(ErrInvalidRefreshToken, err)
	}

	// 成功
	{
		clientMock := ClientMock{}
		clientMock.On(
			"Request",
			"POST",
			client.Endpoints.OAuthEndpoints.token.String(),
			testMock.Anything,
			testMock.Anything,
		).Return(&request.Response{
			Err: nil,
			Response: &http.Response{
				StatusCode: 200,
				Body:       ioutil.NopCloser(strings.NewReader(`{"expires_in":3600,"refresh_token":"new_refresh_token","access_token":"i am token"}`)),
			},
		})
		client.Request = clientMock
		client.Credential = &Credential{
			RefreshToken: "old_refresh_token",
		}
		mock.ExpectBegin()
		mock.ExpectExec("UPDATE(.+)").WillReturnResult(sqlmock.NewResult(1, 1))
		mock.ExpectCommit()
		err := client.UpdateCredential(context.Background(), false)
		clientMock.AssertExpectations(t)
		asserts.NoError(mock.ExpectationsWereMet())
		asserts.NoError(err)
		cacheRes, ok := cache.Get("onedrive_TestClient_UpdateCredential")
		asserts.True(ok)
		cacheCredential := cacheRes.(Credential)
		asserts.Equal("new_refresh_token", cacheCredential.RefreshToken)
		asserts.Equal("i am token", cacheCredential.AccessToken)
	}

	// OneDrive返回错误
	{
		cache.Deletes([]string{"TestClient_UpdateCredential"}, "onedrive_")
		clientMock := ClientMock{}
		clientMock.On(
			"Request",
			"POST",
			client.Endpoints.OAuthEndpoints.token.String(),
			testMock.Anything,
			testMock.Anything,
		).Return(&request.Response{
			Err: nil,
			Response: &http.Response{
				StatusCode: 400,
				Body:       ioutil.NopCloser(strings.NewReader(`{"error":"error"}`)),
			},
		})
		client.Request = clientMock
		client.Credential = &Credential{
			RefreshToken: "old_refresh_token",
		}
		err := client.UpdateCredential(context.Background(), false)
		clientMock.AssertExpectations(t)
		asserts.Error(err)
	}

	// 从缓存中获取
	{
		cache.Set("onedrive_TestClient_UpdateCredential", Credential{
			ExpiresIn:    time.Now().Add(time.Duration(10) * time.Second).Unix(),
			AccessToken:  "AccessToken",
			RefreshToken: "RefreshToken",
		}, 0)
		client.Credential = &Credential{
			RefreshToken: "old_refresh_token",
		}
		err := client.UpdateCredential(context.Background(), false)
		asserts.NoError(err)
		asserts.Equal("AccessToken", client.Credential.AccessToken)
		asserts.Equal("RefreshToken", client.Credential.RefreshToken)
	}

	// 无需重新获取
	{
		client.Credential = &Credential{
			RefreshToken: "old_refresh_token",
			AccessToken:  "AccessToken2",
			ExpiresIn:    time.Now().Add(time.Duration(10) * time.Second).Unix(),
		}
		err := client.UpdateCredential(context.Background(), false)
		asserts.NoError(err)
		asserts.Equal("AccessToken2", client.Credential.AccessToken)
	}

	// slave failed
	{
		mockController := &controllermock.SlaveControllerMock{}
		mockController.On("GetOneDriveToken", testMock.Anything, testMock.Anything).Return("", errors.New("error"))
		client.ClusterController = mockController
		err := client.UpdateCredential(context.Background(), true)
		asserts.Error(err)
	}

	// slave success
	{
		mockController := &controllermock.SlaveControllerMock{}
		mockController.On("GetOneDriveToken", testMock.Anything, testMock.Anything).Return("AccessToken3", nil)
		client.ClusterController = mockController
		err := client.UpdateCredential(context.Background(), true)
		asserts.NoError(err)
		asserts.Equal("AccessToken3", client.Credential.AccessToken)
	}
}
