scouting: Add an endpoint for populating the match schedule

This patch combines the scraping library with the scouting webserver.
There's now also a new end point for the web page (or debug CLI tool)
to ask the server to fetch the match list. The end point is
`/requests/refresh_match_list`.

All the tests are updated. The `cli_test` downloads a 2016 ny_tr match
list that I downloaded from TBA. It should be a decent integration
test as it uses representative data.

Signed-off-by: Philipp Schrader <philipp.schrader@gmail.com>
Change-Id: I6c540590521b00887eb2ddde2a9369875c659551
diff --git a/scouting/webserver/requests/requests.go b/scouting/webserver/requests/requests.go
index b3e03ff..93ee128 100644
--- a/scouting/webserver/requests/requests.go
+++ b/scouting/webserver/requests/requests.go
@@ -1,12 +1,18 @@
 package requests
 
 import (
+	"errors"
 	"fmt"
 	"io"
 	"net/http"
+	"strconv"
+	"strings"
 
 	"github.com/frc971/971-Robot-Code/scouting/db"
+	"github.com/frc971/971-Robot-Code/scouting/scraping"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/error_response"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/refresh_match_list"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/refresh_match_list_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_matches"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_matches_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_data_scouting"
@@ -26,6 +32,8 @@
 type RequestMatchesForTeamResponseT = request_matches_for_team_response.RequestMatchesForTeamResponseT
 type RequestDataScouting = request_data_scouting.RequestDataScouting
 type RequestDataScoutingResponseT = request_data_scouting_response.RequestDataScoutingResponseT
+type RefreshMatchList = refresh_match_list.RefreshMatchList
+type RefreshMatchListResponseT = refresh_match_list_response.RefreshMatchListResponseT
 
 // The interface we expect the database abstraction to conform to.
 // We use an interface here because it makes unit testing easier.
@@ -38,6 +46,8 @@
 	QueryStats(int) ([]db.Stats, error)
 }
 
+type ScrapeMatchList func(int32, string) ([]scraping.Match, error)
+
 // Handles unknown requests. Just returns a 404.
 func unknown(w http.ResponseWriter, req *http.Request) {
 	w.WriteHeader(http.StatusNotFound)
@@ -262,10 +272,114 @@
 	w.Write(builder.FinishedBytes())
 }
 
-func HandleRequests(db Database, scoutingServer server.ScoutingServer) {
+// TODO(phil): Can we turn this into a generic?
+func parseRefreshMatchList(w http.ResponseWriter, buf []byte) (*RefreshMatchList, bool) {
+	success := true
+	defer func() {
+		if r := recover(); r != nil {
+			respondWithError(w, http.StatusBadRequest, fmt.Sprintf("Failed to parse RefreshMatchList: %v", r))
+			success = false
+		}
+	}()
+	result := refresh_match_list.GetRootAsRefreshMatchList(buf, 0)
+	return result, success
+}
+
+func parseTeamKey(teamKey string) (int, error) {
+	// TBA prefixes teams with "frc". Not sure why. Get rid of that.
+	teamKey = strings.TrimPrefix(teamKey, "frc")
+	return strconv.Atoi(teamKey)
+}
+
+// Parses the alliance data from the specified match and returns the three red
+// teams and the three blue teams.
+func parseTeamKeys(match *scraping.Match) ([3]int32, [3]int32, error) {
+	redKeys := match.Alliances.Red.TeamKeys
+	blueKeys := match.Alliances.Blue.TeamKeys
+
+	if len(redKeys) != 3 || len(blueKeys) != 3 {
+		return [3]int32{}, [3]int32{}, errors.New(fmt.Sprintf(
+			"Found %d red teams and %d blue teams.", len(redKeys), len(blueKeys)))
+	}
+
+	var red [3]int32
+	for i, key := range redKeys {
+		team, err := parseTeamKey(key)
+		if err != nil {
+			return [3]int32{}, [3]int32{}, errors.New(fmt.Sprintf(
+				"Failed to parse red %d team '%s' as integer: %v", i+1, key, err))
+		}
+		red[i] = int32(team)
+	}
+	var blue [3]int32
+	for i, key := range blueKeys {
+		team, err := parseTeamKey(key)
+		if err != nil {
+			return [3]int32{}, [3]int32{}, errors.New(fmt.Sprintf(
+				"Failed to parse blue %d team '%s' as integer: %v", i+1, key, err))
+		}
+		blue[i] = int32(team)
+	}
+	return red, blue, nil
+}
+
+type refreshMatchListHandler struct {
+	db     Database
+	scrape ScrapeMatchList
+}
+
+func (handler refreshMatchListHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+	requestBytes, err := io.ReadAll(req.Body)
+	if err != nil {
+		respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
+		return
+	}
+
+	request, success := parseRefreshMatchList(w, requestBytes)
+	if !success {
+		return
+	}
+
+	matches, err := handler.scrape(request.Year(), string(request.EventCode()))
+	if err != nil {
+		respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Faled to scrape match list: ", err))
+		return
+	}
+
+	for _, match := range matches {
+		// Make sure the data is valid.
+		red, blue, err := parseTeamKeys(&match)
+		if err != nil {
+			respondWithError(w, http.StatusInternalServerError, fmt.Sprintf(
+				"TheBlueAlliance data for match %d is malformed: %v", match.MatchNumber, err))
+			return
+		}
+		// Add the match to the database.
+		handler.db.AddToMatch(db.Match{
+			MatchNumber: int32(match.MatchNumber),
+			// TODO(phil): What does Round mean?
+			Round:     1,
+			CompLevel: match.CompLevel,
+			R1:        red[0],
+			R2:        red[1],
+			R3:        red[2],
+			B1:        blue[0],
+			B2:        blue[1],
+			B3:        blue[2],
+		})
+	}
+
+	var response RefreshMatchListResponseT
+	builder := flatbuffers.NewBuilder(1024)
+	builder.Finish((&response).Pack(builder))
+	w.Write(builder.FinishedBytes())
+}
+
+func HandleRequests(db Database, scrape ScrapeMatchList, scoutingServer server.ScoutingServer) {
 	scoutingServer.HandleFunc("/requests", unknown)
 	scoutingServer.Handle("/requests/submit/data_scouting", submitDataScoutingHandler{db})
 	scoutingServer.Handle("/requests/request/all_matches", requestAllMatchesHandler{db})
 	scoutingServer.Handle("/requests/request/matches_for_team", requestMatchesForTeamHandler{db})
 	scoutingServer.Handle("/requests/request/data_scouting", requestDataScoutingHandler{db})
+	scoutingServer.Handle("/requests/refresh_match_list", refreshMatchListHandler{db, scrape})
 }