package main

import (
	"errors"
	"io"
	"io/fs"
	"log"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"launchpad.net/email-reminder/internal/config"
	"launchpad.net/email-reminder/internal/events"
	"launchpad.net/email-reminder/internal/util"
)

var cfg = config.Config{
	MaxRecipientsPerEvent: 10000,
}

var defaultRecipient = util.EmailRecipient{Email: "default@example.com"}

// Hide any log output that processEvent may trigger
func discardLogs(t *testing.T) {
	t.Helper()
	original := log.Writer()
	log.SetOutput(io.Discard)
	t.Cleanup(func() {
		log.SetOutput(original)
	})
}

type mockNotifier struct {
	sendFunc      func(body, subject string, recipients []util.EmailRecipient) []error
	sendDebugFunc func(now time.Time, event events.Event, when string, failures []error, defaultRecipient util.EmailRecipient)
}

func (m *mockNotifier) Send(body, subject string, recipients []util.EmailRecipient) []error {
	if m.sendFunc != nil {
		return m.sendFunc(body, subject, recipients)
	}
	return nil
}

func (m *mockNotifier) SendDebug(now time.Time, event events.Event, when string, failures []error, defaultRecipient util.EmailRecipient) {
	if m.sendDebugFunc != nil {
		m.sendDebugFunc(now, event, when, failures, defaultRecipient)
	}
}

func TestProcessEventSkipsInvalidEvent(t *testing.T) {
	discardLogs(t)

	now := time.Now()
	event := events.Event{Type: events.EventTypeBirthday}

	called := false
	notifier := &mockNotifier{
		sendFunc: func(body, subject string, recipients []util.EmailRecipient) []error {
			called = true
			return nil
		},
	}
	processEvent(now, event, defaultRecipient, cfg, notifier)

	if called {
		t.Fatalf("expected invalid event to be skipped")
	}
}

func TestProcessEventSendsReminderWhenOccurring(t *testing.T) {
	discardLogs(t)

	now := time.Date(2024, time.March, 10, 9, 0, 0, 0, time.UTC)
	event := events.Event{
		Type:      events.EventTypeBirthday,
		Name:      "Alice",
		Month:     time.March,
		Day:       10,
		Reminders: []events.Reminder{{Type: events.ReminderTypeSameDay}},
	}

	sendCount := 0
	debugCount := 0
	notifier := &mockNotifier{
		sendFunc: func(body, subject string, recipients []util.EmailRecipient) []error {
			sendCount++
			if body == "" {
				t.Fatalf("expected body to be populated")
			}
			if subject == "" {
				t.Fatalf("expected subject to be populated")
			}
			if len(recipients) == 0 {
				t.Fatalf("expected at least one recipient")
			}
			return nil
		},
		sendDebugFunc: func(now time.Time, event events.Event, when string, failures []error, defaultRecipient util.EmailRecipient) {
			debugCount++
		},
	}
	processEvent(now, event, defaultRecipient, cfg, notifier)

	if sendCount != 1 {
		t.Fatalf("expected exactly one reminder to be sent, got %d", sendCount)
	}
	if debugCount != 0 {
		t.Fatalf("expected no debug email to be sent")
	}
}

func TestProcessEventSendsDebugOnFailure(t *testing.T) {
	discardLogs(t)

	now := time.Date(2024, time.March, 10, 9, 0, 0, 0, time.UTC)
	event := events.Event{
		Type:      events.EventTypeBirthday,
		Name:      "Alice",
		Month:     time.March,
		Day:       10,
		Reminders: []events.Reminder{{Type: events.ReminderTypeSameDay}},
	}

	sendCount := 0
	debugCount := 0
	notifier := &mockNotifier{
		sendFunc: func(body, subject string, recipients []util.EmailRecipient) []error {
			sendCount++
			return []error{errors.New("boom")}
		},
		sendDebugFunc: func(now time.Time, event events.Event, when string, failures []error, defaultRecipient util.EmailRecipient) {
			debugCount++
			if len(failures) != 1 {
				t.Fatalf("expected to receive the send errors")
			}
		},
	}
	processEvent(now, event, defaultRecipient, cfg, notifier)

	if sendCount != 1 {
		t.Fatalf("expected a send attempt")
	}
	if debugCount != 1 {
		t.Fatalf("expected a debug email to be triggered")
	}
}

func TestProcessEventSkipsWhenNotOccurring(t *testing.T) {
	discardLogs(t)

	now := time.Date(2024, time.March, 5, 9, 0, 0, 0, time.UTC)
	event := events.Event{
		Type:      events.EventTypeBirthday,
		Name:      "Alice",
		Month:     time.March,
		Day:       10,
		Reminders: []events.Reminder{{Type: events.ReminderTypeSameDay}},
	}

	sendCount := 0
	notifier := &mockNotifier{
		sendFunc: func(body, subject string, recipients []util.EmailRecipient) []error {
			sendCount++
			return nil
		},
	}
	processEvent(now, event, defaultRecipient, cfg, notifier)

	if sendCount != 0 {
		t.Fatalf("expected no reminder to be sent, got %d", sendCount)
	}
}

func TestProcessEventHandlesMultipleRemindersAndRecipients(t *testing.T) {
	discardLogs(t)

	now := time.Date(2024, time.March, 7, 9, 0, 0, 0, time.UTC)
	event := events.Event{
		Type:  events.EventTypeBirthday,
		Name:  "Alice",
		Month: time.March,
		Day:   10,
		Reminders: []events.Reminder{
			{Type: events.ReminderTypeDaysBefore, Days: 3},
			{Type: events.ReminderTypeSameDay},
		},
		Recipients: []events.Recipient{
			{Name: "Bob", Email: "bob@example.com"},
			{Name: "Carol", Email: "carol@example.com"},
		},
	}

	sendCount := 0
	debugCount := 0
	var gotBody, gotSubject string
	var gotRecipientsCount int
	notifier := &mockNotifier{
		sendFunc: func(body, subject string, recipients []util.EmailRecipient) []error {
			sendCount++
			gotBody = body
			gotSubject = subject
			gotRecipientsCount = len(recipients)
			return nil
		},
		sendDebugFunc: func(now time.Time, event events.Event, when string, failures []error, defaultRecipient util.EmailRecipient) {
			debugCount++
		},
	}
	processEvent(now, event, defaultRecipient, cfg, notifier)

	if sendCount != 1 {
		t.Fatalf("expected exactly one reminder to be sent, got %d", sendCount)
	}
	if debugCount != 0 {
		t.Fatalf("expected no debug email to be sent, got %d", debugCount)
	}
	if gotSubject == "" {
		t.Fatalf("expected subject to be populated")
	}
	if !strings.Contains(gotBody, "in 3 days") {
		t.Fatalf("expected body to reference the first matching reminder, got %q", gotBody)
	}
	if gotRecipientsCount != 2 {
		t.Fatalf("expected two recipients, got %d", gotRecipientsCount)
	}
}

// Minimal testing mock of fs.DirEntry
type mockDirEntry struct {
	name  string
	isDir bool
}

func (m mockDirEntry) Name() string               { return m.name }
func (m mockDirEntry) IsDir() bool                { return m.isDir }
func (m mockDirEntry) Type() fs.FileMode          { return 0 }
func (m mockDirEntry) Info() (fs.FileInfo, error) { return nil, nil }

func TestShouldProcessPath(t *testing.T) {
	spoolDir := "/var/spool/reminders"

	tests := []struct {
		name        string
		path        string
		dirEntry    fs.DirEntry
		walkErr     error
		wantProcess bool
		wantErr     error
	}{
		{
			name:        "regular file should be processed",
			path:        "/var/spool/reminders/user1",
			dirEntry:    mockDirEntry{name: "user1", isDir: false},
			walkErr:     nil,
			wantProcess: true,
			wantErr:     nil,
		},
		{
			name:        "hidden file should be skipped",
			path:        "/var/spool/reminders/.hidden",
			dirEntry:    mockDirEntry{name: ".hidden", isDir: false},
			walkErr:     nil,
			wantProcess: false,
			wantErr:     nil,
		},
		{
			name:        "hidden directory should be skipped with SkipDir",
			path:        "/var/spool/reminders/.cache",
			dirEntry:    mockDirEntry{name: ".cache", isDir: true},
			walkErr:     nil,
			wantProcess: false,
			wantErr:     fs.SkipDir,
		},
		{
			name:        "regular directory should not be processed but continue",
			path:        "/var/spool/reminders/subdir",
			dirEntry:    mockDirEntry{name: "subdir", isDir: true},
			walkErr:     nil,
			wantProcess: false,
			wantErr:     nil,
		},
		{
			name:        "error on spool dir should be fatal",
			path:        spoolDir,
			dirEntry:    mockDirEntry{name: "reminders", isDir: true},
			walkErr:     os.ErrPermission,
			wantProcess: false,
			wantErr:     os.ErrPermission,
		},
		{
			name:        "error on file should be skipped",
			path:        "/var/spool/reminders/badfile",
			dirEntry:    mockDirEntry{name: "badfile", isDir: false},
			walkErr:     os.ErrPermission,
			wantProcess: false,
			wantErr:     nil,
		},
		{
			name:        "nested regular file should be processed",
			path:        "/var/spool/reminders/subdir/user 2",
			dirEntry:    mockDirEntry{name: "user 2", isDir: false},
			walkErr:     nil,
			wantProcess: true,
			wantErr:     nil,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			discardLogs(t)
			conf := config.Config{}
			gotProcess, gotErr := shouldProcessPath(tt.path, tt.dirEntry, spoolDir, tt.walkErr, conf)

			if gotProcess != tt.wantProcess {
				t.Errorf("process = %v, want %v", gotProcess, tt.wantProcess)
			}
			if tt.wantErr == nil {
				if gotErr != nil {
					t.Errorf("err = %v, want nil", gotErr)
				}
			} else if !errors.Is(gotErr, tt.wantErr) {
				t.Errorf("err = %v, wantErr %v", gotErr, tt.wantErr)
			}
		})
	}
}

func TestProcessSpooledFile(t *testing.T) {
	discardLogs(t)

	// Create a temporary directory and file for testing
	tmpDir := t.TempDir()
	testFile := filepath.Join(tmpDir, "testuser")

	// Write valid XML content
	validContent := `<?xml version="1.0" encoding="UTF-8"?>
<user name="Test User" email="test@example.com">
  <birthday name="Alice" month="3" day="10">
    <reminder type="sameday"/>
  </birthday>
</user>`

	if err := os.WriteFile(testFile, []byte(validContent), 0644); err != nil {
		t.Fatalf("failed to create test file: %v", err)
	}

	now := time.Date(2024, time.March, 5, 9, 0, 0, 0, time.UTC)
	conf := config.Config{}

	// Process the file (which should delete it)
	err := processSpooledFile(testFile, tmpDir, now, conf)
	if err != nil {
		t.Fatalf("processSpoolFile failed: %v", err)
	}

	// Verify file was deleted
	if _, err := os.Stat(testFile); !os.IsNotExist(err) {
		t.Errorf("expected file to be deleted, but it still exists")
	}
}

func TestProcessSpooledFileUnreadable(t *testing.T) {
	discardLogs(t)

	// Use a non-existent file path
	nonExistentFile := "/tmp/this-file-should-not-exist-" + t.Name()

	now := time.Date(2024, time.March, 5, 9, 0, 0, 0, time.UTC)
	conf := config.Config{}

	// Should not return an error for unreadable files (logs and skips instead)
	err := processSpooledFile(nonExistentFile, "/tmp", now, conf)
	if err != nil {
		t.Fatalf("expected no error for unreadable file, got: %v", err)
	}
}

func TestProcessSpooledFileSimulate(t *testing.T) {
	discardLogs(t)

	tmpDir := t.TempDir()
	testFile := filepath.Join(tmpDir, "testuser")

	validContent := `<?xml version="1.0" encoding="UTF-8"?>
<user name="Test User" email="test@example.com">
  <birthday name="Alice" month="3" day="10">
    <reminder type="sameday"/>
  </birthday>
</user>`

	if err := os.WriteFile(testFile, []byte(validContent), 0644); err != nil {
		t.Fatalf("failed to create test file: %v", err)
	}

	now := time.Date(2024, time.March, 5, 9, 0, 0, 0, time.UTC)
	conf := config.Config{Simulate: true}

	err := processSpooledFile(testFile, tmpDir, now, conf)
	if err != nil {
		t.Fatalf("processSpoolFile failed: %v", err)
	}

	// In simulate mode, file should NOT be deleted
	if _, err := os.Stat(testFile); os.IsNotExist(err) {
		t.Errorf("expected file to NOT be deleted in simulate mode")
	}
}

func TestProcessSpooledFilePermissionError(t *testing.T) {
	if os.Getuid() == 0 {
		t.Skip("test requires non-root user")
	}

	discardLogs(t)

	tmpDir := t.TempDir()
	testFile := filepath.Join(tmpDir, "testuser")

	validContent := `<?xml version="1.0" encoding="UTF-8"?>
<user name="Test User" email="test@example.com">
  <birthday name="Alice" month="3" day="10">
    <reminder type="sameday"/>
  </birthday>
</user>`

	if err := os.WriteFile(testFile, []byte(validContent), 0644); err != nil {
		t.Fatalf("failed to create test file: %v", err)
	}

	// Make directory read-only to prevent deletion
	if err := os.Chmod(tmpDir, 0555); err != nil {
		t.Fatalf("failed to change directory permissions: %v", err)
	}
	t.Cleanup(func() { os.Chmod(tmpDir, 0755) })

	now := time.Date(2024, time.March, 5, 9, 0, 0, 0, time.UTC)
	conf := config.Config{}

	// Should return error when unable to delete
	err := processSpooledFile(testFile, tmpDir, now, conf)
	if err == nil {
		t.Fatalf("expected error when unable to delete file")
	}
}
