package drawio_test

import (
	"io"
	"io/fs"
	"os"
	"os/exec"
	"path"
	"strings"
	"testing"
	"time"

	"git.mousesoft.ru/ms/drawio-exporter/pkg/drawio"
	"github.com/stretchr/testify/assert"
)

// Команда оболочки ОС
type command struct {
	cmd  string   // Команда
	args []string // Аргументы команды
}

// Файл с диаграммами
type source struct {
	output   string    // Папка вывода для ExportFile
	data     string    // Данные файла с диаграммами
	diagrams []string  // Срез имён диаграмм
	commands []command // Ожидаемые команды экспорта диаграмм
}

// Элемент папки с файлами
type dirEntry struct {
	name  string // Имя элемента
	isDir bool   // Истина, если элемент является папкой
}

var (
	_ os.DirEntry = (*dirEntry)(nil)
	_ os.FileInfo = (*dirEntry)(nil)
)

func (entry dirEntry) Name() string {
	return entry.name
}

func (entry dirEntry) IsDir() bool {
	return entry.isDir
}

func (entry dirEntry) Type() fs.FileMode {
	if entry.isDir {
		return fs.ModeDir
	}
	return 0
}

func (entry dirEntry) Info() (fs.FileInfo, error) {
	return nil, nil
}

func (entry dirEntry) Size() int64 {
	return 100
}

func (entry dirEntry) Mode() fs.FileMode {
	return entry.Type()
}

func (entry dirEntry) ModTime() time.Time {
	return time.Now()
}

func (entry dirEntry) Sys() any {
	return nil
}

// Конвертация команд в информацию о командах
func cmd2info(commands []*exec.Cmd) []command {
	cmdInfo := make([]command, len(commands))
	for i, cmd := range commands {
		cmdInfo[i] = command{
			cmd:  cmd.Path,
			args: make([]string, len(cmd.Args)),
		}
		copy(cmdInfo[i].args, cmd.Args)
	}
	return cmdInfo
}

type exportTest struct {
	name  string                   // Наименование теста
	dirs  map[string][]os.DirEntry // Папки файлов с диаграммами
	files map[string]source        // Файлы с диаграммами
}

// Информация об ожидаемых командах при экспорте папки
func (test exportTest) dirCommands(dir string, recursive bool) []command {
	commands := make([]command, 0, 4)
	if tc, ok := test.dirs[dir]; ok {
		for _, entry := range tc {
			path := path.Join(dir, entry.Name())
			if entry.IsDir() && recursive {
				commands = append(commands, test.dirCommands(path, true)...)
			} else {
				commands = append(commands, test.files[path].commands...)
			}
		}
	}
	return commands
}

// Тестовые данные
var testData = []exportTest{
	{
		name: "positive case",
		dirs: map[string][]os.DirEntry{
			"source": {
				dirEntry{name: "diagrams.drawio", isDir: false},
			},
		},
		files: map[string]source{
			"source/diagrams.drawio": {
				data: `<mxfile host="test">
  <diagram name="1" id="01">
    <mxGraphModel page="1">
    </mxGraphModel>
  </diagram>
  <diagram name="2" id="02">
    <mxGraphModel page="1">
    </mxGraphModel>
  </diagram>
  <diagram name="3"  id="03">
    <mxGraphModel page="1">
    </mxGraphModel>
  </diagram>
</mxfile>`,
				diagrams: []string{"1", "2", "3"},
				commands: []command{
					{
						cmd: "drawio",
						args: []string{
							"drawio", "--page-index", "0",
							"--output", "export/diagrams-1.pdf",
							"--export", "source/diagrams.drawio",
						},
					},
					{
						cmd: "drawio",
						args: []string{
							"drawio", "--page-index", "1",
							"--output", "export/diagrams-2.pdf",
							"--export", "source/diagrams.drawio",
						},
					},
					{
						cmd: "drawio",
						args: []string{
							"drawio", "--page-index", "2",
							"--output", "export/diagrams-3.pdf",
							"--export", "source/diagrams.drawio",
						},
					},
				},
			},
		},
	},
	{
		name: "invalid source",
		dirs: map[string][]os.DirEntry{
			"source": {
				dirEntry{name: "diagrams.drawio", isDir: false},
			},
		},
		files: map[string]source{
			"source/diagrams.drawio": {
				data: `<nxfile host="test">
<diagram name="1" id="01">
  <mxGraphModel page="1">
  </mxGraphModel>
</diagram>
<diagram name="2" id="02">
  <mxGraphModel page="1">
  </mxGraphModel>
</diagram>
<diagram name="3"  id="03">
  <mxGraphModel page="1">
  </mxGraphModel>
</diagram>
</nxfile>`,
				diagrams: []string{},
			},
		},
	},
	{
		name: "nested dirs",
		dirs: map[string][]os.DirEntry{
			"source": {
				dirEntry{name: "diagrams.drawio", isDir: false},
				dirEntry{name: "subdir", isDir: true},
				dirEntry{name: "additional.xml", isDir: false},
			},
			"source/subdir": {
				dirEntry{name: "Вложенные диаграммы.drawio", isDir: false},
			},
		},
		files: map[string]source{
			"source/diagrams.drawio": {
				data: `<mxfile host="test">
  <diagram name="1" id="01">
    <mxGraphModel page="1">
    </mxGraphModel>
  </diagram>
  <diagram name="2" id="02">
    <mxGraphModel page="1">
    </mxGraphModel>
  </diagram>
  <diagram name="3"  id="03">
    <mxGraphModel page="1">
    </mxGraphModel>
  </diagram>
</mxfile>`,
				diagrams: []string{"1", "2", "3"},
				commands: []command{
					{
						cmd: "drawio",
						args: []string{
							"drawio", "--page-index", "0",
							"--output", "export/diagrams-1.pdf",
							"--export", "source/diagrams.drawio",
						},
					},
					{
						cmd: "drawio",
						args: []string{
							"drawio", "--page-index", "1",
							"--output", "export/diagrams-2.pdf",
							"--export", "source/diagrams.drawio",
						},
					},
					{
						cmd: "drawio",
						args: []string{
							"drawio", "--page-index", "2",
							"--output", "export/diagrams-3.pdf",
							"--export", "source/diagrams.drawio",
						},
					},
				},
			},
			"source/additional.xml": {
				data: `<mxfile host="test">
  <diagram name="Один" id="01">
    <mxGraphModel page="1">
    </mxGraphModel>
  </diagram>
  <diagram name="Два" id="02">
    <mxGraphModel page="1">
    </mxGraphModel>
  </diagram>
</mxfile>`,
				diagrams: []string{"Один", "Два"},
				commands: []command{
					{
						cmd: "drawio",
						args: []string{
							"drawio", "--page-index", "0",
							"--output", "export/additional-Один.pdf",
							"--export", "source/additional.xml",
						},
					},
					{
						cmd: "drawio",
						args: []string{
							"drawio", "--page-index", "1",
							"--output", "export/additional-Два.pdf",
							"--export", "source/additional.xml",
						},
					},
				},
			},
			"source/subdir/Вложенные диаграммы.drawio": {
				output: "export/subdir",
				data: `<mxfile host="test">
  <diagram name="Первая диаграмма" id="01">
    <mxGraphModel page="1">
    </mxGraphModel>
  </diagram>
  <diagram name="Вторая диаграмма" id="02">
    <mxGraphModel page="1">
    </mxGraphModel>
  </diagram>
</mxfile>`,
				diagrams: []string{"Первая диаграмма", "Вторая диаграмма"},
				commands: []command{
					{
						cmd: "drawio",
						args: []string{
							"drawio", "--page-index", "0",
							"--output", "export/subdir/Вложенные диаграммы-Первая диаграмма.pdf",
							"--export", "source/subdir/Вложенные диаграммы.drawio",
						},
					},
					{
						cmd: "drawio",
						args: []string{
							"drawio", "--page-index", "1",
							"--output", "export/subdir/Вложенные диаграммы-Вторая диаграмма.pdf",
							"--export", "source/subdir/Вложенные диаграммы.drawio",
						},
					},
				},
			},
		},
	},
}

// Инициализация тестовых данных
func init() {
	for _, test := range testData {
		for _, src := range test.files {
			for i := range src.commands {
				cmd := exec.Command("drawio")
				src.commands[i].cmd = cmd.Path
			}
		}
	}
}

func TestDiagrams(t *testing.T) {
	for _, test := range testData {
		t.Run(test.name, func(t *testing.T) {
			for filePath, source := range test.files {
				t.Run(filePath, func(t *testing.T) {
					diagrams, err := drawio.Diagrams(strings.NewReader(source.data))
					assert.NoError(t, err)
					assert.ElementsMatch(t, source.diagrams, diagrams)
				})
			}
		})
	}
}

func TestExportFile(t *testing.T) {
	for _, test := range testData {
		t.Run(test.name, func(t *testing.T) {
			for filePath, source := range test.files {
				var openOpt = drawio.WithNop()
				if len(source.output) > 0 {
					openOpt = drawio.WithOutput(source.output)
				}
				t.Run(filePath, func(t *testing.T) {
					var (
						openFileCalls = []string{}
						exp           = drawio.New(
							drawio.WithOpenFile(func(s string) (io.ReadCloser, error) {
								openFileCalls = append(openFileCalls, s)
								return io.NopCloser(strings.NewReader(source.data)), nil
							}),
							openOpt,
						)
					)
					commands, err := exp.ExportFile(filePath, []string{})
					assert.NoError(t, err)
					if assert.Equal(t, 1, len(openFileCalls)) {
						assert.Equal(t, filePath, openFileCalls[0])
					}
					assert.ElementsMatch(t, source.commands, cmd2info(commands))
				})
			}
		})
	}
}

func TestExportDir(t *testing.T) {
	for _, recursive := range []bool{false, true} {
		var (
			name            string
			recursiveOption drawio.Option
		)
		if recursive {
			name = "recursive"
			recursiveOption = drawio.WithRecursive()
		} else {
			name = "non-recursive"
			recursiveOption = drawio.WithNop()
		}
		t.Run(name, func(t *testing.T) {
			for _, test := range testData {
				t.Run(test.name, func(t *testing.T) {
					var (
						openFileCalls = []string{}
						exp           = drawio.New(
							drawio.WithOpenFile(func(s string) (io.ReadCloser, error) {
								openFileCalls = append(openFileCalls, s)
								return io.NopCloser(
									strings.NewReader(test.files[s].data),
								), nil
							}),
							drawio.WithReadDir(func(s string) ([]os.DirEntry, error) {
								return test.dirs[s], nil
							}),
							recursiveOption,
						)
					)
					for dir := range test.dirs {
						pathDir, _ := path.Split(dir)
						if len(pathDir) > 0 {
							continue
						}
						t.Run(dir, func(t *testing.T) {
							openFileCalls = openFileCalls[:0]
							commands, err := exp.ExportDir(dir, []string{})
							assert.NoError(t, err)
							assert.ElementsMatch(t,
								test.dirCommands(dir, recursive), cmd2info(commands))
						})
					}
				})
			}
		})
	}
}
