<?php

namespace Illuminate\Support\Testing\Fakes;

use Illuminate\Support\Collection;
use Illuminate\Container\Container;
use Illuminate\Contracts\Mail\Mailer;
use Illuminate\Contracts\Mail\Mailable;
use PHPUnit_Framework_Assert as PHPUnit;

class MailFake implements Mailer
{
    /**
     * All of the mailables that have been sent.
     *
     * @var array
     */
    protected $mailables = [];

    /**
     * Assert if a mailable was sent based on a truth-test callback.
     *
     * @param  string  $mailable
     * @param  callable|null  $callback
     * @return void
     */
    public function assertSent($mailable, $callback = null)
    {
        PHPUnit::assertTrue(
            $this->sent($mailable, $callback)->count() > 0,
            "The expected [{$mailable}] mailable was not sent."
        );
    }

    /**
     * Assert if a mailable was sent based on a truth-test callback.
     *
     * @param  mixed  $users
     * @param  string  $mailable
     * @param  callable|null  $callback
     * @return void
     */
    public function assertSentTo($users, $mailable, $callback = null)
    {
        $users = $this->formatRecipients($users);

        return $this->assertSent($mailable, function ($mailable, $to) use ($users, $callback) {
            if (! $this->recipientsMatch($users, $this->formatRecipients($to))) {
                return false;
            }

            if (! is_null($callback)) {
                return $callback(...func_get_args());
            }

            return true;
        });
    }

    /**
     * Format the recipients into a collection.
     *
     * @param  mixed  $recipients
     * @return \Illuminate\Support\Collection
     */
    protected function formatRecipients($recipients)
    {
        if ($recipients instanceof Collection) {
            return $recipients;
        }

        return collect(is_array($recipients) ? $recipients : [$recipients]);
    }

    /**
     * Determine if two given recipient lists match.
     *
     * @param  \Illuminate\Support\Collection  $expected
     * @param  \Illuminate\Support\Collection  $recipients
     * @return bool
     */
    protected function recipientsMatch($expected, $recipients)
    {
        $expected = $expected->map(function ($expected) {
            return is_object($expected) ? $expected->email : $expected;
        });

        return $recipients->map(function ($recipient) {
            if (is_array($recipient)) {
                return $recipient['email'];
            }

            return is_object($recipient) ? $recipient->email : $recipient;
        })->diff($expected)->count() === 0;
    }

    /**
     * Determine if a mailable was sent based on a truth-test callback.
     *
     * @param  string  $mailable
     * @param  callable|null  $callback
     * @return void
     */
    public function assertNotSent($mailable, $callback = null)
    {
        PHPUnit::assertTrue(
            $this->sent($mailable, $callback)->count() === 0,
            "The unexpected [{$mailable}] mailable was sent."
        );
    }

    /**
     * Get all of the mailables matching a truth-test callback.
     *
     * @param  string  $mailable
     * @param  callable|null  $callback
     * @return \Illuminate\Support\Collection
     */
    public function sent($mailable, $callback = null)
    {
        if (! $this->hasSent($mailable)) {
            return collect();
        }

        $callback = $callback ?: function () {
            return true;
        };

        return $this->mailablesOf($mailable)->filter(function ($mailable) use ($callback) {
            return $callback($mailable->mailable, ...array_values($mailable->getRecipients()));
        });
    }

    /**
     * Determine if the given mailable has been sent.
     *
     * @param  string  $mailable
     * @return bool
     */
    public function hasSent($mailable)
    {
        return $this->mailablesOf($mailable)->count() > 0;
    }

    /**
     * Get all of the mailed mailables for a given type.
     *
     * @param  string  $type
     * @return \Illuminate\Support\Collection
     */
    protected function mailablesOf($type)
    {
        return collect($this->mailables)->filter(function ($m) use ($type) {
            return $m->mailable instanceof $type;
        });
    }

    /**
     * Begin the process of mailing a mailable class instance.
     *
     * @param  mixed  $users
     * @return MailableMailer
     */
    public function to($users)
    {
        $this->mailables[] = $mailable = (new MailableFake)->to($users);

        return $mailable;
    }

    /**
     * Begin the process of mailing a mailable class instance.
     *
     * @param  mixed  $users
     * @return MailableMailer
     */
    public function bcc($users)
    {
        $this->mailables[] = $mailable = (new MailableFake)->bcc($users);

        return $mailable;
    }

    /**
     * Send a new message when only a raw text part.
     *
     * @param  string  $text
     * @param  \Closure|string  $callback
     * @return int
     */
    public function raw($text, $callback)
    {
        //
    }

    /**
     * Send a new message using a view.
     *
     * @param  string|array  $view
     * @param  array  $data
     * @param  \Closure|string  $callback
     * @return void
     */
    public function send($view, array $data = [], $callback = null)
    {
        if (! $view instanceof Mailable) {
            return;
        }

        Container::getInstance()->call([$view, 'build']);

        $mailable = new MailableFake;

        $mailable->mailable = $view;

        if ($recipients = $view->to) {
            $mailable->to($recipients);
        }

        if ($recipients = $view->bcc) {
            $mailable->bcc($recipients);
        }

        if ($recipients = $view->cc) {
            $mailable->cc($recipients);
        }

        $this->mailables[] = $mailable;
    }

    /**
     * Get the array of failed recipients.
     *
     * @return array
     */
    public function failures()
    {
        //
    }

    /**
     * Queue a new e-mail message for sending.
     *
     * @param  string|array  $view
     * @param  array  $data
     * @param  \Closure|string  $callback
     * @param  string|null  $queue
     * @return mixed
     */
    public function queue($view, array $data = [], $callback = null, $queue = null)
    {
        $this->send($view);
    }
}
