//go:build unittest

package fiat

import (
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"os"
	"reflect"
	"testing"
	"time"

	"github.com/golang/glog"
	"github.com/martinboehm/btcutil/chaincfg"
	"github.com/trezor/blockbook/bchain"
	"github.com/trezor/blockbook/bchain/coins/btc"
	"github.com/trezor/blockbook/common"
	"github.com/trezor/blockbook/db"
)

func TestMain(m *testing.M) {
	// set the current directory to blockbook root so that ./static/ works
	if err := os.Chdir(".."); err != nil {
		glog.Fatal("Chdir error:", err)
	}
	c := m.Run()
	chaincfg.ResetParams()
	os.Exit(c)
}

func setupRocksDB(t *testing.T, parser bchain.BlockChainParser) (*db.RocksDB, *common.InternalState, string) {
	tmp, err := os.MkdirTemp("", "testdb")
	if err != nil {
		t.Fatal(err)
	}
	d, err := db.NewRocksDB(tmp, 100000, -1, parser, nil, false)
	if err != nil {
		t.Fatal(err)
	}
	is, err := d.LoadInternalState("fakecoin")
	if err != nil {
		t.Fatal(err)
	}
	d.SetInternalState(is)
	return d, is, tmp
}

func closeAndDestroyRocksDB(t *testing.T, db *db.RocksDB, dbpath string) {
	// destroy db
	if err := db.Close(); err != nil {
		t.Fatal(err)
	}
	os.RemoveAll(dbpath)
}

type testBitcoinParser struct {
	*btc.BitcoinParser
}

func bitcoinTestnetParser() *btc.BitcoinParser {
	return btc.NewBitcoinParser(
		btc.GetChainParams("test"),
		&btc.Configuration{BlockAddressesToKeep: 1})
}

// getFiatRatesMockData reads a stub JSON response from a file and returns its content as string
func getFiatRatesMockData(name string) (string, error) {
	var filename string
	filename = "fiat/mock_data/" + name + ".json"
	mockFile, err := os.Open(filename)
	if err != nil {
		glog.Errorf("Cannot open file %v", filename)
		return "", err
	}
	b, err := io.ReadAll(mockFile)
	if err != nil {
		glog.Errorf("Cannot read file %v", filename)
		return "", err
	}
	return string(b), nil
}

func TestFiatRates(t *testing.T) {
	d, _, tmp := setupRocksDB(t, &testBitcoinParser{
		BitcoinParser: bitcoinTestnetParser(),
	})
	defer closeAndDestroyRocksDB(t, d, tmp)

	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		var err error
		var mockData string

		if r.URL.Path == "/ping" {
			w.WriteHeader(200)
		} else if r.URL.Path == "/coins/list" {
			mockData, err = getFiatRatesMockData("coinlist")
		} else if r.URL.Path == "/simple/supported_vs_currencies" {
			mockData, err = getFiatRatesMockData("vs_currencies")
		} else if r.URL.Path == "/simple/price" {
			if r.URL.Query().Get("ids") == "ethereum" {
				mockData, err = getFiatRatesMockData("simpleprice_base")
			} else {
				mockData, err = getFiatRatesMockData("simpleprice_tokens")
			}
		} else if r.URL.Path == "/coins/ethereum/market_chart" {
			vsCurrency := r.URL.Query().Get("vs_currency")
			if vsCurrency == "usd" {
				days := r.URL.Query().Get("days")
				if days == "max" {
					mockData, err = getFiatRatesMockData("market_chart_eth_usd_max")
				} else {
					mockData, err = getFiatRatesMockData("market_chart_eth_usd_1")
				}
			} else {
				mockData, err = getFiatRatesMockData("market_chart_eth_other")
			}
		} else if r.URL.Path == "/coins/vendit/market_chart" || r.URL.Path == "/coins/ethereum-cash-token/market_chart" {
			mockData, err = getFiatRatesMockData("market_chart_token_other")
		} else {
			t.Fatalf("Unknown URL path: %v", r.URL.Path)
		}

		if err != nil {
			t.Fatalf("Error loading stub data: %v", err)
		}
		fmt.Fprintln(w, mockData)
	}))
	defer mockServer.Close()

	// mocked CoinGecko API
	configJSON := `{"fiat_rates": "coingecko", "fiat_rates_params": "{\"url\": \"` + mockServer.URL + `\", \"coin\": \"ethereum\",\"platformIdentifier\":\"ethereum\",\"platformVsCurrency\": \"eth\",\"periodSeconds\": 60}"}`

	fiatRates, err := NewFiatRates(d, []byte(configJSON), nil)
	if err != nil {
		t.Fatalf("FiatRates init error: %v", err)
	}

	// get current tickers
	currentTickers, err := fiatRates.downloader.CurrentTickers()
	if err != nil {
		t.Fatalf("Error in CurrentTickers: %v", err)
		return
	}
	if currentTickers == nil {
		t.Fatalf("CurrentTickers returned nil value")
		return
	}

	wantCurrentTickers := common.CurrencyRatesTicker{
		Rates: map[string]float32{
			"aed": 8447.1,
			"ars": 268901,
			"aud": 3314.36,
			"btc": 0.07531005,
			"eth": 1,
			"eur": 2182.99,
			"ltc": 29.097696,
			"usd": 2299.72,
		},
		TokenRates: map[string]float32{
			"0x5e9997684d061269564f94e5d11ba6ce6fa9528c": 5.58195e-07,
			"0x906710835d1ae85275eb770f06873340ca54274b": 1.39852e-10,
		},
		Timestamp: currentTickers.Timestamp,
	}
	if !reflect.DeepEqual(currentTickers, &wantCurrentTickers) {
		t.Fatalf("CurrentTickers() = %v, want %v", *currentTickers, wantCurrentTickers)
	}

	ticker, err := fiatRates.db.FiatRatesFindLastTicker("usd", "")
	if err != nil {
		t.Fatalf("FiatRatesFindLastTicker failed with error: %v", err)
	}
	if ticker != nil {
		t.Fatalf("FiatRatesFindLastTicker found unexpected data")
	}

	// update historical tickers for the first time
	err = fiatRates.downloader.UpdateHistoricalTickers()
	if err != nil {
		t.Fatalf("UpdateHistoricalTickers 1st pass failed with error: %v", err)
	}
	err = fiatRates.downloader.UpdateHistoricalTokenTickers()
	if err != nil {
		t.Fatalf("UpdateHistoricalTokenTickers 1st pass failed with error: %v", err)
	}

	ticker, err = fiatRates.db.FiatRatesFindLastTicker("usd", "")
	if err != nil || ticker == nil {
		t.Fatalf("FiatRatesFindLastTicker failed with error: %v", err)
	}
	wantTicker := common.CurrencyRatesTicker{
		Rates: map[string]float32{
			"aed": 241272.48,
			"ars": 241272.48,
			"aud": 241272.48,
			"btc": 241272.48,
			"eth": 241272.48,
			"eur": 241272.48,
			"ltc": 241272.48,
			"usd": 1794.5397,
		},
		TokenRates: map[string]float32{
			"0x5e9997684d061269564f94e5d11ba6ce6fa9528c": 4.161734e+07,
			"0x906710835d1ae85275eb770f06873340ca54274b": 4.161734e+07,
		},
		Timestamp: time.Unix(1654732800, 0).UTC(),
	}
	if !reflect.DeepEqual(ticker, &wantTicker) {
		t.Fatalf("UpdateHistoricalTickers(usd) 1st pass = %v, want %v", *ticker, wantTicker)
	}

	ticker, err = fiatRates.db.FiatRatesFindLastTicker("eur", "")
	if err != nil || ticker == nil {
		t.Fatalf("FiatRatesFindLastTicker failed with error: %v", err)
	}
	wantTicker = common.CurrencyRatesTicker{
		Rates: map[string]float32{
			"aed": 240402.97,
			"ars": 240402.97,
			"aud": 240402.97,
			"btc": 240402.97,
			"eth": 240402.97,
			"eur": 240402.97,
			"ltc": 240402.97,
		},
		TokenRates: map[string]float32{
			"0x5e9997684d061269564f94e5d11ba6ce6fa9528c": 4.1464476e+07,
			"0x906710835d1ae85275eb770f06873340ca54274b": 4.1464476e+07,
		},
		Timestamp: time.Unix(1654819200, 0).UTC(),
	}
	if !reflect.DeepEqual(ticker, &wantTicker) {
		t.Fatalf("UpdateHistoricalTickers(eur) 1st pass = %v, want %v", *ticker, wantTicker)
	}

	// update historical tickers for the second time
	err = fiatRates.downloader.UpdateHistoricalTickers()
	if err != nil {
		t.Fatalf("UpdateHistoricalTickers 2nd pass failed with error: %v", err)
	}
	err = fiatRates.downloader.UpdateHistoricalTokenTickers()
	if err != nil {
		t.Fatalf("UpdateHistoricalTokenTickers 2nd pass failed with error: %v", err)
	}
	ticker, err = fiatRates.db.FiatRatesFindLastTicker("usd", "")
	if err != nil || ticker == nil {
		t.Fatalf("FiatRatesFindLastTicker failed with error: %v", err)
	}
	wantTicker = common.CurrencyRatesTicker{
		Rates: map[string]float32{
			"aed": 240402.97,
			"ars": 240402.97,
			"aud": 240402.97,
			"btc": 240402.97,
			"eth": 240402.97,
			"eur": 240402.97,
			"ltc": 240402.97,
			"usd": 1788.4183,
		},
		TokenRates: map[string]float32{
			"0x5e9997684d061269564f94e5d11ba6ce6fa9528c": 4.1464476e+07,
			"0x906710835d1ae85275eb770f06873340ca54274b": 4.1464476e+07,
		},
		Timestamp: time.Unix(1654819200, 0).UTC(),
	}
	if !reflect.DeepEqual(ticker, &wantTicker) {
		t.Fatalf("UpdateHistoricalTickers(usd) 2nd pass = %v, want %v", *ticker, wantTicker)
	}
	ticker, err = fiatRates.db.FiatRatesFindLastTicker("eur", "")
	if err != nil || ticker == nil {
		t.Fatalf("FiatRatesFindLastTicker failed with error: %v", err)
	}
	if !reflect.DeepEqual(ticker, &wantTicker) {
		t.Fatalf("UpdateHistoricalTickers(eur) 2nd pass = %v, want %v", *ticker, wantTicker)
	}
}
