package httpinvoce

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	"git.schreifuchs.ch/lou-taylor/accounting/internal/email"
	"git.schreifuchs.ch/lou-taylor/accounting/pkg/invoice"
	"git.schreifuchs.ch/lou-taylor/accounting/pkg/invoice/issue"
	"git.schreifuchs.ch/lou-taylor/accounting/pkg/invoice/model"
	"git.schreifuchs.ch/lou-taylor/accounting/pkg/invoice/report"
)

// Exported versions of the request structs for testing
type InvoiceReq struct {
	Debtor            model.Entity `json:"debtor"`
	Creditor          model.Entity `json:"creditor"`
	DurationThreshold string       `json:"durationThreshold"` // Changed to string
	HourlyRate        float64      `json:"hourlyRate"`
	Repos             []string     `json:"repositories"`
}

func (i InvoiceReq) GetRepos() (repos []invoice.Repo, err error) {
	for i, repo := range i.Repos {
		parts := strings.Split(repo, "/")
		if len(parts) != 2 {
			err = fmt.Errorf("cannot read body: repo with index %d does not split into 2 parts", i)
			return repos, err
		}
		repos = append(repos, invoice.Repo{
			Owner: parts[0],
			Repo:  parts[1],
		})
	}
	return repos, err
}

type SendReq struct {
	To      []string   `json:"to"`
	Cc      []string   `json:"cc"`
	Bcc     []string   `json:"bcc"`
	Subject string     `json:"subject"`
	Body    string     `json:"body"`
	Invoice InvoiceReq `json:"invoice"`
}

func (s SendReq) ToEMail() email.Mail {
	return email.Mail{
		To:      s.To,
		Cc:      s.Cc,
		Bcc:     s.Bcc,
		Subject: s.Subject,
		Body:    s.Body,
	}
}

// MockInvoiceService mocks the invoice.Service interface
type MockInvoiceService struct {
	GenerateFunc func(creditor model.Entity, debtor *model.Entity, durationThreshold time.Duration, hourlyRate float64, repos []invoice.Repo) (io.ReadCloser, *report.Report, error)
}

func (m *MockInvoiceService) Generate(creditor model.Entity, debtor *model.Entity, durationThreshold time.Duration, hourlyRate float64, repos []invoice.Repo) (io.ReadCloser, *report.Report, error) {
	if m.GenerateFunc != nil {
		return m.GenerateFunc(creditor, debtor, durationThreshold, hourlyRate, repos)
	}
	return nil, &report.Report{}, nil
}

// MockEmailService mocks the email.Service interface
type MockEmailService struct {
	SendFunc func(mail email.Mail) error
}

func (m *MockEmailService) Send(mail email.Mail) error {
	if m.SendFunc != nil {
		return m.SendFunc(mail)
	}
	return nil
}

func TestCreateInvoice(t *testing.T) {
	// Create a dummy logger for the service
	dummyLogger := slog.New(slog.NewTextHandler(io.Discard, nil))

	tests := []struct {
		name           string
		requestBody    InvoiceReq
		mockGenerate   func(creditor model.Entity, debtor *model.Entity, durationThreshold time.Duration, hourlyRate float64, repos []invoice.Repo) (io.ReadCloser, *report.Report, error)
		expectedStatus int
		expectedBody   string // For error messages or specific content
	}{
		{
			name: "successful invoice creation",
			requestBody: InvoiceReq{
				Creditor:          model.Entity{Name: "Creditor"},
				Debtor:            model.Entity{Name: "Debtor"},
				DurationThreshold: "1h", // Changed to string
				HourlyRate:        100,
				Repos:             []string{"owner/repo1"},
			},
			mockGenerate: func(creditor model.Entity, debtor *model.Entity, durationThreshold time.Duration, hourlyRate float64, repos []invoice.Repo) (io.ReadCloser, *report.Report, error) {
				pdfContent := "mock PDF content"
				// Create a report with a positive total duration
				mockReport := report.New([]issue.Issue{{Duration: time.Hour}}, model.Entity{}, &model.Entity{}, 0)
				return io.NopCloser(bytes.NewReader([]byte(pdfContent))), mockReport, nil
			},
			expectedStatus: http.StatusOK,
			expectedBody:   "mock PDF content",
		},
		{
			name: "invalid request body",
			requestBody: InvoiceReq{ // Malformed request body
				Creditor:          model.Entity{Name: "Creditor"},
				Debtor:            model.Entity{Name: "Debtor"},
				DurationThreshold: "invalid", // Changed to string
				HourlyRate:        100,
				Repos:             []string{"owner/repo1"},
			},
			mockGenerate:   nil, // Not called for invalid body
			expectedStatus: http.StatusBadRequest,
			expectedBody:   "cannot read body", // Partial match for error message
		},
		{
			name: "invoice generation error",
			requestBody: InvoiceReq{
				Creditor:          model.Entity{Name: "Creditor"},
				Debtor:            model.Entity{Name: "Debtor"},
				DurationThreshold: "1h", // Changed to string
				HourlyRate:        100,
				Repos:             []string{"owner/repo1"},
			},
			mockGenerate: func(creditor model.Entity, debtor *model.Entity, durationThreshold time.Duration, hourlyRate float64, repos []invoice.Repo) (io.ReadCloser, *report.Report, error) {
				return nil, nil, errors.New("failed to generate invoice")
			},
			expectedStatus: http.StatusInternalServerError,
			expectedBody:   "internal server error",
		},
		{
			name: "no suitable issues to be billed",
			requestBody: InvoiceReq{
				Creditor:          model.Entity{Name: "Creditor"},
				Debtor:            model.Entity{Name: "Debtor"},
				DurationThreshold: "1h", // Changed to string
				HourlyRate:        100,
				Repos:             []string{"owner/repo1"},
			},
			mockGenerate: func(creditor model.Entity, debtor *model.Entity, durationThreshold time.Duration, hourlyRate float64, repos []invoice.Repo) (io.ReadCloser, *report.Report, error) {
				// Create a report with zero total duration
				mockReport := report.New([]issue.Issue{}, model.Entity{}, &model.Entity{}, 0)
				return io.NopCloser(bytes.NewReader([]byte("mock PDF content"))), mockReport, nil
			},
			expectedStatus: http.StatusNotFound,
			expectedBody:   "no suitable issues to be billed",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			mockInvoiceService := &MockInvoiceService{
				GenerateFunc: tt.mockGenerate,
			}
			service := Service{invoice: mockInvoiceService, log: dummyLogger} // Pass the dummy logger

			reqBody, _ := json.Marshal(tt.requestBody)
			fmt.Println("Request Body:", string(reqBody)) // Debug print
			req := httptest.NewRequest(http.MethodPost, "/invoice", bytes.NewBuffer(reqBody))
			rr := httptest.NewRecorder()

			service.createInvoice(rr, req)

			if rr.Code != tt.expectedStatus {
				t.Errorf("expected status %d, got %d", tt.expectedStatus, rr.Code)
			}

			body := rr.Body.String()
			if tt.expectedBody != "" && !bytes.Contains([]byte(body), []byte(tt.expectedBody)) {
				t.Errorf("expected body to contain %q, got %q", tt.expectedBody, body)
			}
		})
	}
}

func TestSendInvoice(t *testing.T) {
	// Create a dummy logger for the service
	dummyLogger := slog.New(slog.NewTextHandler(io.Discard, nil))

	tests := []struct {
		name           string
		requestBody    SendReq
		mockGenerate   func(creditor model.Entity, debtor *model.Entity, durationThreshold time.Duration, hourlyRate float64, repos []invoice.Repo) (io.ReadCloser, *report.Report, error)
		mockSend       func(mail email.Mail) error
		expectedStatus int
		expectedBody   string
	}{
		{
			name: "successful invoice send",
			requestBody: SendReq{
				To:      []string{"test@example.com"},
				Subject: "Test Invoice",
				Body:    "Here is your invoice.",
				Invoice: InvoiceReq{
					Creditor:          model.Entity{Name: "Creditor"},
					Debtor:            model.Entity{Name: "Debtor"},
					DurationThreshold: "1h", // Changed to string
					HourlyRate:        100,
					Repos:             []string{"owner/repo1"},
				},
			},
			mockGenerate: func(creditor model.Entity, debtor *model.Entity, durationThreshold time.Duration, hourlyRate float64, repos []invoice.Repo) (io.ReadCloser, *report.Report, error) {
				pdfContent := "mock PDF content for send"
				mockReport := report.New([]issue.Issue{{Duration: time.Hour}}, model.Entity{}, &model.Entity{}, 0)
				return io.NopCloser(bytes.NewReader([]byte(pdfContent))), mockReport, nil
			},
			mockSend: func(mail email.Mail) error {
				return nil // Simulate successful email send
			},
			expectedStatus: http.StatusOK,
			expectedBody:   "mock PDF content for send",
		},
		{
			name: "invoice generation error during send",
			requestBody: SendReq{
				To:      []string{"test@example.com"},
				Subject: "Test Invoice",
				Body:    "Here is your invoice.",
				Invoice: InvoiceReq{
					Creditor:          model.Entity{Name: "Creditor"},
					Debtor:            model.Entity{Name: "Debtor"},
					DurationThreshold: "1h", // Changed to string
					HourlyRate:        100,
					Repos:             []string{"owner/repo1"},
				},
			},
			mockGenerate: func(creditor model.Entity, debtor *model.Entity, durationThreshold time.Duration, hourlyRate float64, repos []invoice.Repo) (io.ReadCloser, *report.Report, error) {
				return nil, nil, errors.New("failed to generate invoice for send")
			},
			mockSend:       nil, // Not called
			expectedStatus: http.StatusInternalServerError,
			expectedBody:   "error while processing invoice:",
		},
		{
			name: "email send error",
			requestBody: SendReq{
				To:      []string{"test@example.com"},
				Subject: "Test Invoice",
				Body:    "Here is your invoice.",
				Invoice: InvoiceReq{
					Creditor:          model.Entity{Name: "Creditor"},
					Debtor:            model.Entity{Name: "Debtor"},
					DurationThreshold: "1h", // Changed to string
					HourlyRate:        100,
					Repos:             []string{"owner/repo1"},
				},
			},
			mockGenerate: func(creditor model.Entity, debtor *model.Entity, durationThreshold time.Duration, hourlyRate float64, repos []invoice.Repo) (io.ReadCloser, *report.Report, error) {
				pdfContent := "mock PDF content for send"
				mockReport := report.New([]issue.Issue{{Duration: time.Hour}}, model.Entity{}, &model.Entity{}, 0)
				return io.NopCloser(bytes.NewReader([]byte(pdfContent))), mockReport, nil
			},
			mockSend: func(mail email.Mail) error {
				return errors.New("failed to send email")
			},
			expectedStatus: http.StatusInternalServerError,
			expectedBody:   "error while sending mail:",
		},
		{
			name: "no suitable issues to be billed during send",
			requestBody: SendReq{
				To:      []string{"test@example.com"},
				Subject: "Test Invoice",
				Body:    "Here is your invoice.",
				Invoice: InvoiceReq{
					Creditor:          model.Entity{Name: "Creditor"},
					Debtor:            model.Entity{Name: "Debtor"},
					DurationThreshold: "1h", // Changed to string
					HourlyRate:        100,
					Repos:             []string{"owner/repo1"},
				},
			},
			mockGenerate: func(creditor model.Entity, debtor *model.Entity, durationThreshold time.Duration, hourlyRate float64, repos []invoice.Repo) (io.ReadCloser, *report.Report, error) {
				mockReport := report.New([]issue.Issue{}, model.Entity{}, &model.Entity{}, 0)
				return io.NopCloser(bytes.NewReader([]byte("mock PDF content"))), mockReport, nil
			},
			expectedStatus: http.StatusNotFound,
			expectedBody:   "no suitable issues to be billed",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			mockInvoiceService := &MockInvoiceService{
				GenerateFunc: tt.mockGenerate,
			}
			mockEmailService := &MockEmailService{
				SendFunc: tt.mockSend,
			}
			service := Service{invoice: mockInvoiceService, mail: mockEmailService, log: dummyLogger} // Pass the dummy logger

			reqBody, _ := json.Marshal(tt.requestBody)
			fmt.Println("Request Body:", string(reqBody)) // Debug print
			req := httptest.NewRequest(http.MethodPost, "/invoice/send", bytes.NewBuffer(reqBody))
			rr := httptest.NewRecorder()

			service.sendInvoice(rr, req)

			if rr.Code != tt.expectedStatus {
				t.Errorf("expected status %d, got %d", tt.expectedStatus, rr.Code)
			}

			body := rr.Body.String()
			if tt.expectedBody != "" && !bytes.Contains([]byte(body), []byte(tt.expectedBody)) {
				t.Errorf("expected body to contain %q, got %q", tt.expectedBody, body)
			}
		})
	}
}
