// Copyright 2014 The go-github AUTHORS. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// This tool tests for the JSON mappings in the go-github data types. It will
// identify fields that are returned by the live GitHub API, but that are not
// currently mapped into a struct field of the relevant go-github type. This
// helps to ensure that all relevant data returned by the API is being made
// accessible, particularly new fields that are periodically (and sometimes
// quietly) added to the API over time.
//
// These tests simply aid in identifying which fields aren't being mapped; it
// is not necessarily true that every one of them should always be mapped.
// Some fields may be undocumented for a reason, either because they aren't
// actually used yet or should not be relied upon.
package main

import (
	"context"
	"encoding/json"
	"flag"
	"fmt"
	"os"
	"reflect"
	"strings"

	"github.com/google/go-github/v38/github"
	"golang.org/x/oauth2"
)

var (
	client *github.Client

	// auth indicates whether tests are being run with an OAuth token.
	// Tests can use this flag to skip certain tests when run without auth.
	auth bool

	skipURLs = flag.Bool("skip_urls", false, "skip url fields")
)

func main() {
	flag.Parse()

	token := os.Getenv("GITHUB_AUTH_TOKEN")
	if token == "" {
		print("!!! No OAuth token. Some tests won't run. !!!\n\n")
		client = github.NewClient(nil)
	} else {
		tc := oauth2.NewClient(context.Background(), oauth2.StaticTokenSource(
			&oauth2.Token{AccessToken: token},
		))
		client = github.NewClient(tc)
		auth = true
	}

	for _, tt := range []struct {
		url string
		typ interface{}
	}{
		//{"rate_limit", &github.RateLimits{}},
		{"users/octocat", &github.User{}},
		{"user", &github.User{}},
		{"users/willnorris/keys", &[]github.Key{}},
		{"orgs/google-test", &github.Organization{}},
		{"repos/google/go-github", &github.Repository{}},
		{"repos/google/go-github/issues/1", &github.Issue{}},
		{"/gists/9257657", &github.Gist{}},
	} {
		err := testType(tt.url, tt.typ)
		if err != nil {
			fmt.Printf("error: %v\n", err)
		}
	}
}

// testType fetches the JSON resource at urlStr and compares its keys to the
// struct fields of typ.
func testType(urlStr string, typ interface{}) error {
	slice := reflect.Indirect(reflect.ValueOf(typ)).Kind() == reflect.Slice

	req, err := client.NewRequest("GET", urlStr, nil)
	if err != nil {
		return err
	}

	// start with a json.RawMessage so we can decode multiple ways below
	raw := new(json.RawMessage)
	_, err = client.Do(context.Background(), req, raw)
	if err != nil {
		return err
	}

	// unmarshal directly to a map
	var m1 map[string]interface{}
	if slice {
		var s []map[string]interface{}
		err = json.Unmarshal(*raw, &s)
		if err != nil {
			return err
		}
		m1 = s[0]
	} else {
		err = json.Unmarshal(*raw, &m1)
		if err != nil {
			return err
		}
	}

	// unmarshal to typ first, then re-marshal and unmarshal to a map
	err = json.Unmarshal(*raw, typ)
	if err != nil {
		return err
	}

	var byt []byte
	if slice {
		// use first item in slice
		v := reflect.Indirect(reflect.ValueOf(typ))
		byt, err = json.Marshal(v.Index(0).Interface())
		if err != nil {
			return err
		}
	} else {
		byt, err = json.Marshal(typ)
		if err != nil {
			return err
		}
	}

	var m2 map[string]interface{}
	err = json.Unmarshal(byt, &m2)
	if err != nil {
		return err
	}

	// now compare the two maps
	for k, v := range m1 {
		if *skipURLs && strings.HasSuffix(k, "_url") {
			continue
		}
		if _, ok := m2[k]; !ok {
			fmt.Printf("%v missing field for key: %v (example value: %v)\n", reflect.TypeOf(typ), k, v)
		}
	}

	return nil
}
