package cmd

import (
	"context"
	"fmt"
	"os"
	"path/filepath"
	"testing"

	"github.com/pingcap-inc/alioth/pkg/util"
	"github.com/pingcap-inc/alioth/pkg/worker"
	logpattern_go_proto "github.com/pingcap-inc/alioth/proto"
	log_reporter "github.com/pingcap-inc/alioth/reporter"
	log_scanner "github.com/pingcap-inc/alioth/scanner"
	"github.com/pingcap-inc/alioth/storage"
	. "github.com/pingcap/check"
)

func TestClient(t *testing.T) {
	TestingT(t)
}

var _ = Suite(&testLogScannerSuite{})

type testLogScannerSuite struct {
}

func (t *testLogScannerSuite) TestLogScan(c *C) {
	storePath := "../test/dm_pattern"
	var logs []string

	err := filepath.Walk("../test/dm_logs", func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}

		if !info.IsDir() {
			if filepath.Ext(path) == ".log" {
				absPath, err := filepath.Abs(path)
				if err != nil {
					return err
				}
				logs = append(logs, absPath)
			}
		}
		return nil
	})
	c.Assert(err, IsNil)

	store, err := storage.NewStore(storePath)
	c.Assert(err, IsNil)

	testLogScan(c, store, logs)
	testCoverage(c, store)
}

func testCoverage(c *C, store *storage.LogPatternStore) {
	cov, err := log_reporter.NewCoverager(store, "")
	c.Assert(err, IsNil)
	total, covCount := cov.OverallCoverage()
	c.Assert(total, Equals, 252)
	c.Assert(covCount, Equals, 6)

	expectedRes := testGenerateLogScanresult()
	for _, detail := range cov.Details {
		if detail.Coverage != nil && detail.Coverage.CovCount > 0 {
			pos := fmt.Sprintf("%s:%s:%d", detail.Coverage.Pos.PackageInfo.Repo, detail.Coverage.Pos.FilePath, detail.Coverage.Pos.Pos.Line)
			expect := expectedRes[pos]
			c.Assert(expect, NotNil)
			c.Assert(detail.Coverage.CovCount, Equals, expect.cov)
			c.Assert(detail.Coverage.CovCountByLog, DeepEquals, expect.covByFile)
			c.Assert(detail.Pattern.Level, DeepEquals, expect.level)
			if len(expect.message) > 0 {
				c.Assert(detail.Pattern.Signature[0], DeepEquals, expect.message)
			}
			delete(expectedRes, pos)
		}
	}
	c.Assert(expectedRes, HasLen, 0)

	expectUnknown := testGenerateUnknown()
	store.ScanUnknownLog(context.Background(), func(key, value []byte) error {
		lp := &logpattern_go_proto.UnknowLogPattern{}
		err := lp.Unmarshal(value)
		c.Assert(err, IsNil)
		pos := util.PosToStr(lp.Pos)
		expect := expectUnknown[pos]
		c.Assert(expect, NotNil)
		c.Assert(lp.CovCount, Equals, expect.cov)
		c.Assert(lp.CovCountByLog, DeepEquals, expect.covByFile)
		c.Assert(lp.Level, DeepEquals, expect.level)
		c.Assert(lp.Message, DeepEquals, expect.message)
		delete(expectUnknown, pos)
		return nil
	})
	c.Assert(expectUnknown, HasLen, 0)
}

func testLogScan(c *C, store *storage.LogPatternStore, logs []string) {
	rule, err := util.GetLogPatternRule(store)
	c.Assert(err, IsNil)
	c.Assert(rule, DeepEquals, &logpattern_go_proto.LogPatternRule{
		LogLevel: []string{"error"},
	})

	p, err := log_scanner.NewLogProcessor(store, logs)
	c.Assert(err, IsNil)

	c.Assert(p.Run(context.Background(), rule, worker.NewPool(3, "scan")), IsNil)
}

type testLogScanResult struct {
	position  string
	cov       int32
	level     string
	message   string
	covByFile map[string]int32
}

func testGenerateUnknown() map[string]*testLogScanResult {
	res := make(map[string]*testLogScanResult)

	cov1 := &testLogScanResult{
		position: "yyy.go:1004:0",
		cov:      1,
		level:    "ERROR",
		message:  "[yyy.go:1004] [2021/12/02 23:24:43.168 +00:00] [ERROR] [\"different case-sensitive config between task config and source config, use `true` for it.\"]",
		covByFile: map[string]int32{
			"/Users/ianz/Work/go/src/github.com/pingcap-inc/alioth/test/dm_logs/dm-worker-1.log": 1,
		},
	}
	res[cov1.position] = cov1

	cov2 := &testLogScanResult{
		position: "xxx.go:1004:0",
		cov:      2,
		level:    "ERROR",
		message:  "[xxx.go:1004] [2021/12/02 23:24:43.168 +00:00] [ERROR] [\"different case-sensitive config between task config and source config, use `true` for it.\"]",
		covByFile: map[string]int32{
			"/Users/ianz/Work/go/src/github.com/pingcap-inc/alioth/test/dm_logs/dm-worker-1.log": 1,
			"/Users/ianz/Work/go/src/github.com/pingcap-inc/alioth/test/dm_logs/dm-master-2.log": 1,
		},
	}
	res[cov2.position] = cov2
	return res
}

func testGenerateLogScanresult() map[string]*testLogScanResult {
	res := make(map[string]*testLogScanResult)

	cov6 := &testLogScanResult{
		position: "github.com/pingcap/ticdc:dm/syncer/syncer.go:1656",
		cov:      1,
		level:    "error",
		message:  "\"fail to fetch binlog\"",
		covByFile: map[string]int32{
			"/Users/ianz/Work/go/src/github.com/pingcap-inc/alioth/test/dm_logs/dm-worker-2.log": 1,
		},
	}
	res[cov6.position] = cov6

	cov5 := &testLogScanResult{
		position: "github.com/pingcap/ticdc:dm/syncer/streamer_controller.go:287",
		cov:      29,
		level:    "error",
		message:  "\"meet error when get binlog event\"",
		covByFile: map[string]int32{
			"/Users/ianz/Work/go/src/github.com/pingcap-inc/alioth/test/dm_logs/dm-worker-1.log": 13,
			"/Users/ianz/Work/go/src/github.com/pingcap-inc/alioth/test/dm_logs/dm-worker-2.log": 3,
			"/Users/ianz/Work/go/src/github.com/pingcap-inc/alioth/test/dm_logs/dm-worker-3.log": 13,
		},
	}
	res[cov5.position] = cov5

	cov4 := &testLogScanResult{
		position: "github.com/pingcap/ticdc:dm/pkg/conn/baseconn.go:184",
		cov:      64,
		level:    "error",
		message:  "\"execute statement failed\"",
		covByFile: map[string]int32{
			"/Users/ianz/Work/go/src/github.com/pingcap-inc/alioth/test/dm_logs/dm-worker-0.log": 28,
			"/Users/ianz/Work/go/src/github.com/pingcap-inc/alioth/test/dm_logs/dm-worker-1.log": 4,
			"/Users/ianz/Work/go/src/github.com/pingcap-inc/alioth/test/dm_logs/dm-worker-2.log": 31,
			"/Users/ianz/Work/go/src/github.com/pingcap-inc/alioth/test/dm_logs/dm-worker-3.log": 1,
		},
	}
	res[cov4.position] = cov4

	cov3 := &testLogScanResult{
		position: "github.com/pingcap/ticdc:dm/loader/db.go:181",
		cov:      44,
		level:    "error",
		message:  "\"execute statements failed after retry\"",
		covByFile: map[string]int32{
			"/Users/ianz/Work/go/src/github.com/pingcap-inc/alioth/test/dm_logs/dm-worker-0.log": 22,
			"/Users/ianz/Work/go/src/github.com/pingcap-inc/alioth/test/dm_logs/dm-worker-2.log": 22,
		},
	}
	res[cov3.position] = cov3

	cov2 := &testLogScanResult{
		position: "github.com/pingcap/ticdc:dm/dm/worker/subtask.go:331",
		cov:      256,
		level:    "error",
		message:  "\"unit process error\"",
		covByFile: map[string]int32{
			"/Users/ianz/Work/go/src/github.com/pingcap-inc/alioth/test/dm_logs/dm-worker-0.log": 53,
			"/Users/ianz/Work/go/src/github.com/pingcap-inc/alioth/test/dm_logs/dm-worker-1.log": 56,
			"/Users/ianz/Work/go/src/github.com/pingcap-inc/alioth/test/dm_logs/dm-worker-2.log": 89,
			"/Users/ianz/Work/go/src/github.com/pingcap-inc/alioth/test/dm_logs/dm-worker-3.log": 58,
		},
	}
	res[cov2.position] = cov2

	cov1 := &testLogScanResult{
		position: "github.com/pingcap/ticdc:dm/dm/worker/metrics.go:138",
		cov:      6,
		level:    "error",
		message:  "\"status server returned\"",
		covByFile: map[string]int32{
			"/Users/ianz/Work/go/src/github.com/pingcap-inc/alioth/test/dm_logs/dm-worker-0.log": 3,
			"/Users/ianz/Work/go/src/github.com/pingcap-inc/alioth/test/dm_logs/dm-worker-1.log": 2,
			"/Users/ianz/Work/go/src/github.com/pingcap-inc/alioth/test/dm_logs/dm-worker-2.log": 1,
		},
	}
	res[cov1.position] = cov1

	return res
}
