package report

import (
	"archive/zip"
	"bytes"
	"encoding/base64"
	"io"
	"mime"
	"mime/multipart"
	"net/mail"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"
)

var sampleResults = []StepResult{
	{Num: "01", Name: "Bloatware", Status: "OK", Elapsed: 3 * time.Second},
	{Num: "08", Name: "Activation", Status: "ERROR", Elapsed: 2 * time.Second},
}

// writeLog writes a temp Deploy.log and returns its path.
func writeLog(t *testing.T, content []byte) string {
	t.Helper()
	p := filepath.Join(t.TempDir(), "Deploy.log")
	if err := os.WriteFile(p, content, 0644); err != nil {
		t.Fatal(err)
	}
	return p
}

// buildHTML must embed the direct issue link and the per-machine tracker filter.
func TestBuildHTMLLinks(t *testing.T) {
	issueURL := "https://git.xetup.x9.cz/x9/xetup-runs/issues/42"
	html := buildHTML(sampleResults, "PC-TEST", "2026-07-28 10:00", issueURL, "PC-TEST-Deploy.log.zip")

	if !strings.Contains(html, issueURL) {
		t.Errorf("html does not link the run issue %q", issueURL)
	}
	if !strings.Contains(html, "xetup-runs/issues?q=PC-TEST") {
		t.Errorf("html missing per-machine tracker filter link; got:\n%s", html)
	}
}

// With no issue (clean run / post failed), only the tracker link is shown.
func TestBuildHTMLNoIssue(t *testing.T) {
	html := buildHTML(sampleResults, "PC-TEST", "2026-07-28 10:00", "", "PC-TEST-Deploy.log.zip")
	if strings.Contains(html, "Chyby tohoto behu") {
		t.Error("issue-specific line shown when issueURL is empty")
	}
	if !strings.Contains(html, "xetup-runs/issues?q=PC-TEST") {
		t.Error("tracker filter link missing")
	}
}

// The footer must only advertise the attachment when one is actually present.
func TestBuildHTMLFooterMatchesAttachment(t *testing.T) {
	with := buildHTML(sampleResults, "PC-TEST", "2026-07-28 10:00", "", "PC-TEST-Deploy.log.zip")
	if !strings.Contains(with, "v priloze (PC-TEST-Deploy.log.zip)") {
		t.Error("footer should name the attachment when one is present")
	}
	without := buildHTML(sampleResults, "PC-TEST", "2026-07-28 10:00", "", "")
	if strings.Contains(without, "v priloze") {
		t.Errorf("footer claims an attachment when none is attached:\n%s", without)
	}
	if !strings.Contains(without, "log lokalne") {
		t.Error("footer should still point to the local log copy")
	}
}

// zipLog must produce a valid zip whose Deploy.log entry round-trips.
func TestZipLogRoundTrip(t *testing.T) {
	content := []byte("line one\nERROR something failed\nline three\n")
	name, data := zipLog(writeLog(t, content), "PC-TEST")

	if name != "PC-TEST-Deploy.log.zip" {
		t.Errorf("unexpected attachment name %q", name)
	}
	zr, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
	if err != nil {
		t.Fatalf("attachment is not a valid zip: %v", err)
	}
	if len(zr.File) != 1 || zr.File[0].Name != "Deploy.log" {
		t.Fatalf("zip should contain exactly Deploy.log, got %v", zr.File)
	}
	rc, _ := zr.File[0].Open()
	got, _ := io.ReadAll(rc)
	rc.Close()
	if !bytes.Equal(got, content) {
		t.Errorf("zip content mismatch: got %q want %q", got, content)
	}
}

// zipLog returns no attachment when the log is unreadable (email still sends).
func TestZipLogMissing(t *testing.T) {
	name, data := zipLog(filepath.Join(t.TempDir(), "nope.log"), "PC-TEST")
	if name != "" || data != nil {
		t.Errorf("expected no attachment for missing log, got name=%q len=%d", name, len(data))
	}
}

// buildMessage must yield a parseable multipart/mixed message: an HTML part and
// a base64 application/zip attachment that unzips back to the log.
func TestBuildMessageMultipart(t *testing.T) {
	logContent := []byte("deploy log body\nERROR boom\n")
	_, zipData := zipLog(writeLog(t, logContent), "PC-TEST")
	html := buildHTML(sampleResults, "PC-TEST", "2026-07-28 10:00", "https://x/issues/1", "PC-TEST-Deploy.log.zip")

	raw := buildMessage("xetup report PC-TEST", html, "PC-TEST-Deploy.log.zip", zipData)

	m, err := mail.ReadMessage(bytes.NewReader(raw))
	if err != nil {
		t.Fatalf("message does not parse: %v", err)
	}
	mediaType, params, err := mime.ParseMediaType(m.Header.Get("Content-Type"))
	if err != nil || mediaType != "multipart/mixed" {
		t.Fatalf("expected multipart/mixed, got %q (%v)", mediaType, err)
	}

	mr := multipart.NewReader(m.Body, params["boundary"])
	var sawHTML, sawZip bool
	for {
		p, err := mr.NextPart()
		if err == io.EOF {
			break
		}
		if err != nil {
			t.Fatalf("part read error: %v", err)
		}
		ct := p.Header.Get("Content-Type")
		body, _ := io.ReadAll(p)
		switch {
		case strings.HasPrefix(ct, "text/html"):
			sawHTML = true
			if !strings.Contains(string(body), "xetup report") {
				t.Error("html part missing report content")
			}
		case strings.HasPrefix(ct, "application/zip"):
			sawZip = true
			if enc := p.Header.Get("Content-Transfer-Encoding"); enc != "base64" {
				t.Errorf("zip part not base64, got %q", enc)
			}
			// multipart does not auto-decode base64; strip wraps and decode.
			clean := strings.NewReplacer("\r", "", "\n", "").Replace(string(body))
			dec, err := base64.StdEncoding.DecodeString(clean)
			if err != nil {
				t.Fatalf("attachment base64 decode failed: %v", err)
			}
			if _, err := zip.NewReader(bytes.NewReader(dec), int64(len(dec))); err != nil {
				t.Errorf("decoded attachment is not a zip: %v", err)
			}
		}
	}
	if !sawHTML || !sawZip {
		t.Errorf("missing parts: html=%v zip=%v", sawHTML, sawZip)
	}
}

// Plain message (no attachment) must be single-part text/html.
func TestBuildMessagePlain(t *testing.T) {
	raw := buildMessage("subj", "<html>x</html>", "", nil)
	m, err := mail.ReadMessage(bytes.NewReader(raw))
	if err != nil {
		t.Fatalf("plain message does not parse: %v", err)
	}
	if ct := m.Header.Get("Content-Type"); !strings.HasPrefix(ct, "text/html") {
		t.Errorf("expected text/html, got %q", ct)
	}
}
