package report

import (
	"encoding/json"
	"fmt"
	"strings"
	"time"

	"github.com/olekukonko/tablewriter"
	"github.com/pkg/errors"

	"github.com/wallarm/gotestwaf/internal/db"
)

// The minimum length of each column in a console table report.
const colMinWidth = 21

// RenderConsoleReport prints a console report in selected format.
func RenderConsoleReport(
	s *db.Statistics,
	reportTime time.Time,
	wafName string,
	url string,
	args []string,
	ignoreUnresolved bool,
	format string,
) error {
	switch format {
	case consoleReportTextFormat:
		printConsoleReportTable(s, reportTime, wafName, ignoreUnresolved)
	case consoleReportJsonFormat:
		err := printConsoleReportJson(s, reportTime, wafName, url, args)
		if err != nil {
			return err
		}
	default:
		return fmt.Errorf("unknown report format: %s", format)
	}

	return nil
}

// printConsoleReportTable prepare and prints a console report in tabular format.
func printConsoleReportTable(
	s *db.Statistics,
	reportTime time.Time,
	wafName string,
	ignoreUnresolved bool,
) {
	baseHeader := []string{"Test set", "Test case", "Percentage, %", "Blocked", "Bypassed"}
	if !ignoreUnresolved {
		baseHeader = append(baseHeader, "Unresolved")
	}
	baseHeader = append(baseHeader, "Sent", "Failed")

	var buffer strings.Builder

	fmt.Fprintf(&buffer, "True-Positive Tests:\n")

	// Negative cases summary table
	table := tablewriter.NewWriter(&buffer)
	table.Header(baseHeader)

	for _, row := range s.TruePositiveTests.SummaryTable {
		rowAppend := []string{
			row.TestSet,
			row.TestCase,
			fmt.Sprintf("%.2f", row.Percentage),
			fmt.Sprintf("%d", row.Blocked),
			fmt.Sprintf("%d", row.Bypassed),
		}
		if !ignoreUnresolved {
			rowAppend = append(rowAppend, fmt.Sprintf("%d", row.Unresolved))
		}
		rowAppend = append(rowAppend,
			fmt.Sprintf("%d", row.Sent),
			fmt.Sprintf("%d", row.Failed),
		)
		table.Append(rowAppend)
	}

	footerNegativeTests := []string{
		fmt.Sprintf("Date:\n%s", reportTime.Format("2006-01-02")),
		fmt.Sprintf("Project Name:\n%s", wafName),
		fmt.Sprintf("True-Positive Score:\n%.2f%%", s.TruePositiveTests.ResolvedBlockedRequestsPercentage),
		fmt.Sprintf("Blocked (Resolved):\n%d/%d (%.2f%%)",
			s.TruePositiveTests.ReqStats.BlockedRequestsNumber,
			s.TruePositiveTests.ReqStats.ResolvedRequestsNumber,
			s.TruePositiveTests.ResolvedBlockedRequestsPercentage,
		),
		fmt.Sprintf("Bypassed (Resolved):\n%d/%d (%.2f%%)",
			s.TruePositiveTests.ReqStats.BypassedRequestsNumber,
			s.TruePositiveTests.ReqStats.ResolvedRequestsNumber,
			s.TruePositiveTests.ResolvedBypassedRequestsPercentage,
		),
	}
	if !ignoreUnresolved {
		footerNegativeTests = append(footerNegativeTests,
			fmt.Sprintf("Unresolved (Sent):\n%d/%d (%.2f%%)",
				s.TruePositiveTests.ReqStats.UnresolvedRequestsNumber,
				s.TruePositiveTests.ReqStats.AllRequestsNumber,
				s.TruePositiveTests.UnresolvedRequestsPercentage,
			),
		)
	}
	footerNegativeTests = append(footerNegativeTests,
		fmt.Sprintf("Total Sent:\n%d", s.TruePositiveTests.ReqStats.AllRequestsNumber),
		fmt.Sprintf("Failed (Total):\n%d/%d (%.2f%%)",
			s.TruePositiveTests.ReqStats.FailedRequestsNumber,
			s.TruePositiveTests.ReqStats.AllRequestsNumber,
			s.TruePositiveTests.FailedRequestsPercentage,
		),
	)

	table.Footer(footerNegativeTests)
	table.Render()

	fmt.Fprintf(&buffer, "\nTrue-Negative Tests:\n")

	// Positive cases summary table
	posTable := tablewriter.NewWriter(&buffer)
	posTable.Header(baseHeader)

	for _, row := range s.TrueNegativeTests.SummaryTable {
		rowAppend := []string{
			row.TestSet,
			row.TestCase,
			fmt.Sprintf("%.2f", row.Percentage),
			fmt.Sprintf("%d", row.Blocked),
			fmt.Sprintf("%d", row.Bypassed),
		}
		if !ignoreUnresolved {
			rowAppend = append(rowAppend, fmt.Sprintf("%d", row.Unresolved))
		}
		rowAppend = append(rowAppend,
			fmt.Sprintf("%d", row.Sent),
			fmt.Sprintf("%d", row.Failed),
		)
		posTable.Append(rowAppend)
	}

	footerPositiveTests := []string{
		fmt.Sprintf("Date:\n%s", reportTime.Format("2006-01-02")),
		fmt.Sprintf("Project Name:\n%s", wafName),
		fmt.Sprintf("True-Negative Score:\n%.2f%%", s.TrueNegativeTests.ResolvedBypassedRequestsPercentage),
		fmt.Sprintf("Blocked (Resolved):\n%d/%d (%.2f%%)",
			s.TrueNegativeTests.ReqStats.BlockedRequestsNumber,
			s.TrueNegativeTests.ReqStats.ResolvedRequestsNumber,
			s.TrueNegativeTests.ResolvedBlockedRequestsPercentage,
		),
		fmt.Sprintf("Bypassed (Resolved):\n%d/%d (%.2f%%)",
			s.TrueNegativeTests.ReqStats.BypassedRequestsNumber,
			s.TrueNegativeTests.ReqStats.ResolvedRequestsNumber,
			s.TrueNegativeTests.ResolvedBypassedRequestsPercentage,
		),
	}
	if !ignoreUnresolved {
		footerPositiveTests = append(footerPositiveTests,
			fmt.Sprintf("Unresolved (Sent):\n%d/%d (%.2f%%)",
				s.TrueNegativeTests.ReqStats.UnresolvedRequestsNumber,
				s.TrueNegativeTests.ReqStats.AllRequestsNumber,
				s.TrueNegativeTests.UnresolvedRequestsPercentage,
			),
		)
	}
	footerPositiveTests = append(footerPositiveTests,
		fmt.Sprintf("Total Sent:\n%d", s.TrueNegativeTests.ReqStats.AllRequestsNumber),
		fmt.Sprintf("Failed (Total):\n%d/%d (%.2f%%)",
			s.TrueNegativeTests.ReqStats.FailedRequestsNumber,
			s.TrueNegativeTests.ReqStats.AllRequestsNumber,
			s.TrueNegativeTests.FailedRequestsPercentage,
		),
	)

	posTable.Footer(footerPositiveTests)
	posTable.Render()

	fmt.Fprintf(&buffer, "\nSummary:\n")

	// summary table
	sumTable := tablewriter.NewWriter(&buffer)
	baseHeader = []string{"Type", "True-Positive tests blocked", "True-Negative tests passed", "Average"}
	sumTable.Header(baseHeader)

	row := []string{"API Security"}
	if s.Score.ApiSec.TruePositive != -1.0 {
		row = append(row, fmt.Sprintf("%.2f%%", s.Score.ApiSec.TruePositive))
	} else {
		row = append(row, "n/a")
	}
	if s.Score.ApiSec.TrueNegative != -1.0 {
		row = append(row, fmt.Sprintf("%.2f%%", s.Score.ApiSec.TrueNegative))
	} else {
		row = append(row, "n/a")
	}
	if s.Score.ApiSec.Average != -1.0 {
		row = append(row, fmt.Sprintf("%.2f%%", s.Score.ApiSec.Average))
	} else {
		row = append(row, "n/a")
	}
	sumTable.Append(row)

	row = []string{"Application Security"}
	if s.Score.AppSec.TruePositive != -1.0 {
		row = append(row, fmt.Sprintf("%.2f%%", s.Score.AppSec.TruePositive))
	} else {
		row = append(row, "n/a")
	}
	if s.Score.AppSec.TrueNegative != -1.0 {
		row = append(row, fmt.Sprintf("%.2f%%", s.Score.AppSec.TrueNegative))
	} else {
		row = append(row, "n/a")
	}
	if s.Score.AppSec.Average != -1.0 {
		row = append(row, fmt.Sprintf("%.2f%%", s.Score.AppSec.Average))
	} else {
		row = append(row, "n/a")
	}
	sumTable.Append(row)

	footer := []string{"", "", "Score"}
	if s.Score.Average != -1.0 {
		footer = append(footer, fmt.Sprintf("%.2f%%", s.Score.Average))
	} else {
		footer = append(footer, "n/a")
	}
	sumTable.Footer(footer)
	sumTable.Render()

	fmt.Println(buffer.String())
}

// printConsoleReportJson prepares and prints a console report in json format.
func printConsoleReportJson(
	s *db.Statistics,
	reportTime time.Time,
	wafName string,
	url string,
	args []string,
) error {
	report := jsonReport{
		Date:        reportTime.Format(time.ANSIC),
		ProjectName: wafName,
		URL:         url,
		TestCasesFP: s.TestCasesFingerprint,
		Args:        strings.Join(args, " "),
		Score:       s.Score.Average,
	}

	if len(s.TruePositiveTests.SummaryTable) != 0 {
		report.TruePositiveTests = &testsInfo{
			Score: s.TruePositiveTests.ResolvedBlockedRequestsPercentage,
			Summary: requestStats{
				TotalSent:       s.TruePositiveTests.ReqStats.AllRequestsNumber,
				ResolvedTests:   s.TruePositiveTests.ReqStats.ResolvedRequestsNumber,
				BlockedTests:    s.TruePositiveTests.ReqStats.BlockedRequestsNumber,
				BypassedTests:   s.TruePositiveTests.ReqStats.BypassedRequestsNumber,
				UnresolvedTests: s.TruePositiveTests.ReqStats.UnresolvedRequestsNumber,
				FailedTests:     s.TruePositiveTests.ReqStats.FailedRequestsNumber,
			},
			ApiSecStat: requestStats{
				TotalSent:       s.TruePositiveTests.ApiSecReqStats.AllRequestsNumber,
				ResolvedTests:   s.TruePositiveTests.ApiSecReqStats.ResolvedRequestsNumber,
				BlockedTests:    s.TruePositiveTests.ApiSecReqStats.BlockedRequestsNumber,
				BypassedTests:   s.TruePositiveTests.ApiSecReqStats.BypassedRequestsNumber,
				UnresolvedTests: s.TruePositiveTests.ApiSecReqStats.UnresolvedRequestsNumber,
				FailedTests:     s.TruePositiveTests.ApiSecReqStats.FailedRequestsNumber,
			},
			AppSecStat: requestStats{
				TotalSent:       s.TruePositiveTests.AppSecReqStats.AllRequestsNumber,
				ResolvedTests:   s.TruePositiveTests.AppSecReqStats.ResolvedRequestsNumber,
				BlockedTests:    s.TruePositiveTests.AppSecReqStats.BlockedRequestsNumber,
				BypassedTests:   s.TruePositiveTests.AppSecReqStats.BypassedRequestsNumber,
				UnresolvedTests: s.TruePositiveTests.AppSecReqStats.UnresolvedRequestsNumber,
				FailedTests:     s.TruePositiveTests.AppSecReqStats.FailedRequestsNumber,
			},
			TestSets: make(testSets),
		}
		for _, row := range s.TruePositiveTests.SummaryTable {
			if report.TruePositiveTests.TestSets[row.TestSet] == nil {
				report.TruePositiveTests.TestSets[row.TestSet] = make(testCases)
			}
			report.TruePositiveTests.TestSets[row.TestSet][row.TestCase] = &testCaseInfo{
				Percentage: row.Percentage,
				Sent:       row.Sent,
				Blocked:    row.Blocked,
				Bypassed:   row.Bypassed,
				Unresolved: row.Unresolved,
				Failed:     row.Failed,
			}
		}
	}

	if len(s.TrueNegativeTests.SummaryTable) != 0 {
		report.TrueNegativeTests = &testsInfo{
			Score: s.TrueNegativeTests.ResolvedBypassedRequestsPercentage,
			Summary: requestStats{
				TotalSent:       s.TrueNegativeTests.ReqStats.AllRequestsNumber,
				ResolvedTests:   s.TrueNegativeTests.ReqStats.ResolvedRequestsNumber,
				BlockedTests:    s.TrueNegativeTests.ReqStats.BlockedRequestsNumber,
				BypassedTests:   s.TrueNegativeTests.ReqStats.BypassedRequestsNumber,
				UnresolvedTests: s.TrueNegativeTests.ReqStats.UnresolvedRequestsNumber,
				FailedTests:     s.TrueNegativeTests.ReqStats.FailedRequestsNumber,
			},
			ApiSecStat: requestStats{
				TotalSent:       s.TrueNegativeTests.ApiSecReqStats.AllRequestsNumber,
				ResolvedTests:   s.TrueNegativeTests.ApiSecReqStats.ResolvedRequestsNumber,
				BlockedTests:    s.TrueNegativeTests.ApiSecReqStats.BlockedRequestsNumber,
				BypassedTests:   s.TrueNegativeTests.ApiSecReqStats.BypassedRequestsNumber,
				UnresolvedTests: s.TrueNegativeTests.ApiSecReqStats.UnresolvedRequestsNumber,
				FailedTests:     s.TrueNegativeTests.ApiSecReqStats.FailedRequestsNumber,
			},
			AppSecStat: requestStats{
				TotalSent:       s.TrueNegativeTests.AppSecReqStats.AllRequestsNumber,
				ResolvedTests:   s.TrueNegativeTests.AppSecReqStats.ResolvedRequestsNumber,
				BlockedTests:    s.TrueNegativeTests.AppSecReqStats.BlockedRequestsNumber,
				BypassedTests:   s.TrueNegativeTests.AppSecReqStats.BypassedRequestsNumber,
				UnresolvedTests: s.TrueNegativeTests.AppSecReqStats.UnresolvedRequestsNumber,
				FailedTests:     s.TrueNegativeTests.AppSecReqStats.FailedRequestsNumber,
			},
			TestSets: make(testSets),
		}
		for _, row := range s.TrueNegativeTests.SummaryTable {
			if report.TrueNegativeTests.TestSets[row.TestSet] == nil {
				report.TrueNegativeTests.TestSets[row.TestSet] = make(testCases)
			}
			report.TrueNegativeTests.TestSets[row.TestSet][row.TestCase] = &testCaseInfo{
				Percentage: row.Percentage,
				Sent:       row.Sent,
				Blocked:    row.Blocked,
				Bypassed:   row.Bypassed,
				Unresolved: row.Unresolved,
				Failed:     row.Failed,
			}
		}
	}

	jsonBytes, err := json.Marshal(report)
	if err != nil {
		return errors.Wrap(err, "couldn't export report to JSON")
	}

	fmt.Println(string(jsonBytes))

	return nil
}
