package main

import (
	"bytes"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/matrix-org/go-neb/clients"
	"github.com/matrix-org/go-neb/database"
	"maunium.net/go/mautrix/crypto"
	"maunium.net/go/mautrix/crypto/olm"
	mevt "maunium.net/go/mautrix/event"
	"maunium.net/go/mautrix/id"
)

func setupMockServer() (*http.ServeMux, *matrixTripper, *httptest.ResponseRecorder, chan string) {
	mux := http.NewServeMux()
	mxTripper := newMatrixTripper()
	setup(envVars{
		BaseURL:      "http://go.neb",
		DatabaseType: "sqlite3",
		DatabaseURL:  ":memory:",
	}, mux, &http.Client{
		Transport: mxTripper,
	})

	mxTripper.ClearHandlers()
	mockWriter := httptest.NewRecorder()
	reqChan := make(chan string)
	mxTripper.HandlePOSTFilter("@link:hyrule")
	mxTripper.Handle("GET", "/_matrix/client/r0/sync",
		func(req *http.Request) (*http.Response, error) {
			if _, ok := req.URL.Query()["since"]; !ok {
				return newResponse(200, `{"next_batch":"11_22_33_44", "rooms": {}}`), nil
			}
			reqBody := <-reqChan
			return newResponse(200, reqBody), nil
		},
	)
	return mux, mxTripper, mockWriter, reqChan
}

func TestConfigureClient(t *testing.T) {
	mux, _, mockWriter, _ := setupMockServer()

	mockReq, _ := http.NewRequest("POST", "http://go.neb/admin/configureClient", bytes.NewBufferString(`
	{
		"UserID":"@link:hyrule",
		"HomeserverURL":"http://hyrule.loz",
		"AccessToken":"dangeroustogoalone",
		"Sync":true,
		"AutoJoinRooms":true
	}`))
	mux.ServeHTTP(mockWriter, mockReq)
	expectCode := 200
	if mockWriter.Code != expectCode {
		t.Errorf("TestConfigureClient wanted HTTP status %d, got %d", expectCode, mockWriter.Code)
	}
}

func TestRespondToEcho(t *testing.T) {
	mux, mxTripper, mockWriter, reqChan := setupMockServer()

	mxTripper.Handle("POST", "/_matrix/client/r0/keys/upload", func(req *http.Request) (*http.Response, error) {
		return newResponse(200, `{}`), nil
	})

	var joinedRoom string
	var joinedRoomBody []byte
	mxTripper.Handle("POST", "/_matrix/client/r0/join/*", func(req *http.Request) (*http.Response, error) {
		parts := strings.Split(req.URL.String(), "/")
		joinedRoom = parts[len(parts)-1]
		joinedRoomBody, _ = ioutil.ReadAll(req.Body)
		return newResponse(200, `{}`), nil
	})

	var roomMsgBody []byte
	mxTripper.Handle("PUT", "/_matrix/client/r0/rooms/!greatdekutree:hyrule/send/m.room.message/*", func(req *http.Request) (*http.Response, error) {
		roomMsgBody, _ = ioutil.ReadAll(req.Body)
		return newResponse(200, `{}`), nil
	})

	// configure the client
	clientConfigReq, _ := http.NewRequest("POST", "http://go.neb/admin/configureClient", bytes.NewBufferString(`
	{
		"UserID":"@link:hyrule",
		"HomeserverURL":"http://hyrule.loz",
		"AccessToken":"dangeroustogoalone",
		"Sync":true,
		"AutoJoinRooms":true
	}`))
	mux.ServeHTTP(mockWriter, clientConfigReq)

	// configure the echo service
	serviceConfigReq, _ := http.NewRequest("POST", "http://go.neb/admin/configureService", bytes.NewBufferString(`
	{
		"Type": "echo",
		"Id": "test_echo_service",
		"UserID": "@link:hyrule",
		"Config": {}
	}`))

	mux.ServeHTTP(mockWriter, serviceConfigReq)

	// send neb an invite to a room
	reqChan <- `{
		"next_batch":"11_22_33_44",
		"rooms": {
			"invite": {
				"!greatdekutree:hyrule": {"invite_state": {"events": [{
					"type": "m.room.member",
					"sender": "@navi:hyrule",
					"content": {"membership": "invite"},
					"state_key": "@link:hyrule",
					"origin_server_ts": 10000,
					"unsigned": {"age": 100},
					"event_id": "evt123"
				}]}}}
		}
	}`

	// wait for it to be processed
	reqChan <- `{"next_batch":"11_22_33_44", "rooms": {}}`

	expectedRoom := "%21greatdekutree:hyrule"
	if joinedRoom != expectedRoom {
		t.Errorf("Expected join for room %v, got %v", expectedRoom, joinedRoom)
	}
	if expectedBody := `{"inviter":"@navi:hyrule"}`; string(joinedRoomBody) != expectedBody {
		t.Errorf("Expected join message body to be %v, got %v", expectedBody, string(joinedRoomBody))
	}

	// send neb an !echo message
	reqChan <- `{
		"next_batch":"11_22_33_44",
		"rooms": {
			"join": {
				"!greatdekutree:hyrule": {"timeline": {"events": [{
					"type": "m.room.message",
					"sender": "@navi:hyrule",
					"content": {"body": "!echo save zelda", "msgtype": "m.text"},
					"origin_server_ts": 10000,
					"unsigned": {"age": 100},
					"event_id": "evt124"
				}]}}}
		}
	}`

	// wait for it to be processed
	reqChan <- `{"next_batch":"11_22_33_44", "rooms": {}}`

	if expectedEchoResp := `{"msgtype":"m.notice","body":"save zelda"}`; string(roomMsgBody) != expectedEchoResp {
		t.Errorf("Expected echo response to be `%v`, got `%v`", expectedEchoResp, string(roomMsgBody))
	}
}

func TestEncryptedRespondToEcho(t *testing.T) {
	mux, mxTripper, mockWriter, reqChan := setupMockServer()

	// create the two accounts, inbound and outbound sessions, both the bot and mock ones
	accountMock := olm.NewAccount()
	accountBot := olm.NewAccount()
	signingKeyMock, identityKeyMock := accountMock.IdentityKeys()
	signingKeyBot, identityKeyBot := accountBot.IdentityKeys()
	// encryptionEvtContent := &mevt.EncryptionEventContent{Algorithm: "m.megolm.v1.aes-sha2"}
	ogsBot := crypto.NewOutboundGroupSession("!greatdekutree:hyrule", nil)
	ogsBot.Shared = true
	igsMock, err := crypto.NewInboundGroupSession(identityKeyBot, signingKeyBot, "!greatdekutree:hyrule", ogsBot.Internal.Key())
	if err != nil {
		t.Errorf("Error creating mock IGS: %v", err)
	}
	ogsMock := crypto.NewOutboundGroupSession("!greatdekutree:hyrule", nil)
	ogsMock.Shared = true
	igsBot, err := crypto.NewInboundGroupSession(identityKeyMock, signingKeyMock, "!greatdekutree:hyrule", ogsMock.Internal.Key())
	if err != nil {
		t.Errorf("Error creating bot IGS: %v", err)
	}

	mxTripper.Handle("POST", "/_matrix/client/r0/keys/upload", func(req *http.Request) (*http.Response, error) {
		return newResponse(200, `{}`), nil
	})

	mxTripper.Handle("POST", "/_matrix/client/r0/keys/query", func(req *http.Request) (*http.Response, error) {
		return newResponse(200, `{}`), nil
	})

	var joinedRoom string
	var joinedRoomBody []byte
	mxTripper.Handle("POST", "/_matrix/client/r0/join/*", func(req *http.Request) (*http.Response, error) {
		parts := strings.Split(req.URL.String(), "/")
		joinedRoom = parts[len(parts)-1]
		joinedRoomBody, _ = ioutil.ReadAll(req.Body)
		return newResponse(200, `{}`), nil
	})

	var decryptedMsg string
	mxTripper.Handle("PUT", "/_matrix/client/r0/rooms/!greatdekutree:hyrule/send/m.room.encrypted/*", func(req *http.Request) (*http.Response, error) {
		encryptedMsg, _ := ioutil.ReadAll(req.Body)
		var encryptedContent mevt.EncryptedEventContent
		encryptedContent.UnmarshalJSON(encryptedMsg)
		decryptedMsgBytes, _, err := igsMock.Internal.Decrypt(encryptedContent.MegolmCiphertext)
		if err != nil {
			t.Errorf("Error decrypting message sent by bot: %v", err)
		}
		decryptedMsg = string(decryptedMsgBytes)
		return newResponse(200, `{}`), nil
	})

	// configure the client
	clientConfigReq, _ := http.NewRequest("POST", "http://go.neb/admin/configureClient", bytes.NewBufferString(`
	{
		"UserID":"@link:hyrule",
		"DeviceID":"mastersword",
		"HomeserverURL":"http://hyrule.loz",
		"AccessToken":"dangeroustogoalone",
		"Sync":true,
		"AutoJoinRooms":true
	}`))
	mux.ServeHTTP(mockWriter, clientConfigReq)

	// configure the echo service
	serviceConfigReq, _ := http.NewRequest("POST", "http://go.neb/admin/configureService", bytes.NewBufferString(`
	{
		"Type": "echo",
		"Id": "test_echo_service",
		"UserID": "@link:hyrule",
		"Config": {}
	}`))
	mux.ServeHTTP(mockWriter, serviceConfigReq)

	// send neb an invite to a room
	reqChan <- `{
		"next_batch":"11_22_33_44",
		"rooms": {
			"invite": {
				"!greatdekutree:hyrule": {"invite_state": {"events": [{
					"type": "m.room.member",
					"sender": "@navi:hyrule",
					"content": {"membership": "invite"},
					"state_key": "@link:hyrule",
					"origin_server_ts": 10000,
					"unsigned": {"age": 100},
					"event_id": "evt123"
				}]}}}
		}
	}`

	// wait for it to be processed
	reqChan <- `{"next_batch":"11_22_33_44", "rooms": {}}`

	expectedRoom := "%21greatdekutree:hyrule"
	if joinedRoom != expectedRoom {
		t.Errorf("Expected join for room %v, got %v", expectedRoom, joinedRoom)
	}
	if expectedBody := `{"inviter":"@navi:hyrule"}`; string(joinedRoomBody) != expectedBody {
		t.Errorf("Expected join message body to be %v, got %v", expectedBody, string(joinedRoomBody))
	}

	// send neb the room state: encrypted with one member
	reqChan <- `{
		"next_batch":"11_22_33_44",
		"rooms": {
			"join": {"!greatdekutree:hyrule": {"timeline": {"events": [{
					"type": "m.room.encryption",
					"state_key": "",
					"content": {
						"algorithm": "m.megolm.v1.aes-sha2"
					}
				}, {
					"type": "m.room.member",
					"sender": "@navi:hyrule",
					"content": {"membership": "join", "displayname": "Navi"},
					"state_key": "@navi:hyrule",
					"origin_server_ts": 100,
					"event_id": "evt124"
				}]}}}
		}
	}`

	// wait for it to be processed
	reqChan <- `{"next_batch":"11_22_33_44", "rooms": {}}`

	// DB is initialized, store the megolm sessions from before for the bot to be able to decrypt and encrypt
	sqlDB, dialect := database.GetServiceDB().(*database.ServiceDB).GetSQLDb()
	cryptoStore := crypto.NewSQLCryptoStore(sqlDB, dialect, "@link:hyrule-mastersword", "mastersword", []byte("masterswordpickle"), clients.CryptoMachineLogger{})
	if err := cryptoStore.AddOutboundGroupSession(ogsBot); err != nil {
		t.Errorf("Error storing bot OGS: %v", err)
	}

	if err := cryptoStore.PutGroupSession("!greatdekutree:hyrule", identityKeyMock, igsBot.ID(), igsBot); err != nil {
		t.Errorf("Error storing bot IGS: %v", err)
	}
	cryptoStore.PutDevices("@navi:hyrule", map[id.DeviceID]*crypto.DeviceIdentity{
		"NAVI": {
			UserID:      "@navi:hyrule",
			DeviceID:    "NAVI",
			IdentityKey: identityKeyMock,
			SigningKey:  signingKeyMock,
		},
	})

	plaintext := `{"room_id":"!greatdekutree:hyrule","type":"m.room.message","content":{"body":"!echo save zelda","msgtype":"m.text"}}`
	ciphertext, err := ogsMock.Encrypt([]byte(plaintext))
	if err != nil {
		t.Errorf("Error encrypting bytes: %v", err)
	}

	// send neb an !echo message, encrypted with our mock OGS which it has an IGS for
	reqChan <- fmt.Sprintf(`{
		"next_batch":"11_22_33_44",
		"rooms": {
			"join": {"!greatdekutree:hyrule": {"timeline": {"events": [{
					"type": "m.room.encrypted",
					"sender": "@navi:hyrule",
					"content": {
						"algorithm":"m.megolm.v1.aes-sha2",
						"sender_key":"%s",
						"ciphertext":"%s",
						"session_id":"%s",
						"device_id": "NAVI"
					},
					"origin_server_ts": 10000,
					"unsigned": {"age": 100},
					"event_id": "evt125"
				}]}}}
		}
	}`, identityKeyMock, string(ciphertext), ogsMock.ID())

	// wait for it to be processed
	reqChan <- `{"next_batch":"11_22_33_44", "rooms": {}}`

	expectedDecryptedMsg := `{"room_id":"!greatdekutree:hyrule","type":"m.room.message","content":{"msgtype":"m.notice","body":"save zelda"}}`
	if decryptedMsg != expectedDecryptedMsg {
		t.Errorf("Expected decrypted message to be `%v`, got `%v`", expectedDecryptedMsg, decryptedMsg)
	}
}