from catalyst.core.callback import Callback, CallbackNode, CallbackOrder
from catalyst.core.runner import IRunner


class CheckRunCallback(Callback):
    """@TODO: Docs. Contribution is welcome."""

    def __init__(self, num_batch_steps: int = 3, num_epoch_steps: int = 2):
        """@TODO: Docs. Contribution is welcome."""
        super().__init__(order=CallbackOrder.external, node=CallbackNode.all)
        self.num_batch_steps = num_batch_steps
        self.num_epoch_steps = num_epoch_steps

    def on_epoch_end(self, runner: IRunner):
        """@TODO: Docs. Contribution is welcome."""
        if runner.epoch >= self.num_epoch_steps:
            runner.need_early_stop = True

    def on_batch_end(self, runner: IRunner):
        """@TODO: Docs. Contribution is welcome."""
        if runner.loader_batch_step >= self.num_batch_steps:
            runner.need_early_stop = True


class EarlyStoppingCallback(Callback):
    """@TODO: Docs. Contribution is welcome."""

    def __init__(
        self,
        patience: int,
        metric: str = "loss",
        minimize: bool = True,
        min_delta: float = 1e-6,
    ):
        """@TODO: Docs. Contribution is welcome."""
        super().__init__(order=CallbackOrder.external, node=CallbackNode.all)
        self.best_score = None
        self.metric = metric
        self.patience = patience
        self.num_bad_epochs = 0
        self.is_better = None

        if minimize:
            self.is_better = lambda score, best: score <= (best - min_delta)
        else:
            self.is_better = lambda score, best: score >= (best + min_delta)

    def on_epoch_end(self, runner: IRunner) -> None:
        """@TODO: Docs. Contribution is welcome."""
        if runner.stage_name.startswith("infer"):
            return

        score = runner.valid_metrics[self.metric]
        if self.best_score is None:
            self.best_score = score
        if self.is_better(score, self.best_score):
            self.num_bad_epochs = 0
            self.best_score = score
        else:
            self.num_bad_epochs += 1

        if self.num_bad_epochs >= self.patience:
            print(f"Early stop at {runner.epoch} epoch")
            runner.need_early_stop = True

catalyst/core/callbacks/early_stop.py
from catalyst.core.callback import Callback, CallbackNode, CallbackOrder
from catalyst.core.runner import IRunner


class CheckRunCallback(Callback):
    """@TODO: Docs. Contribution is welcome."""

    def __init__(self, num_batch_steps: int = 3, num_epoch_steps: int = 2):
        """@TODO: Docs. Contribution is welcome."""
        super().__init__(order=CallbackOrder.external, node=CallbackNode.all)
        self.num_batch_steps = num_batch_steps
        self.num_epoch_steps = num_epoch_steps

    def on_epoch_end(self, runner: IRunner):
        """@TODO: Docs. Contribution is welcome."""
        if runner.epoch >= self.num_epoch_steps:
            runner.need_early_stop = True

    def on_batch_end(self, runner: IRunner):
        """@TODO: Docs. Contribution is welcome."""
        if runner.loader_batch_step >= self.num_batch_steps:
            runner.need_early_stop = True


class EarlyStoppingCallback(Callback):
    """@TODO: Docs. Contribution is welcome."""

    def __init__(
        self,
        patience: int,
        metric: str = "loss",
        minimize: bool = True,
        min_delta: float = 1e-6,
    ):
        """@TODO: Docs. Contribution is welcome."""
        super().__init__(order=CallbackOrder.external, node=CallbackNode.all)
        self.best_score = None
        self.metric = metric
        self.patience = patience
        self.num_bad_epochs = 0
        self.is_better = None

        if minimize:
            self.is_better = lambda score, best: score <= (best - min_delta)
        else:
            self.is_better = lambda score, best: score >= (best + min_delta)

    def on_epoch_end(self, runner: IRunner) -> None:
        """@TODO: Docs. Contribution is welcome."""
        if runner.stage_name.startswith("infer"):
            return

        score = runner.valid_metrics[self.metric]
        if self.best_score is None or self.is_better(score, self.best_score):
            self.num_bad_epochs = 0
            self.best_score = score
        else:
            self.num_bad_epochs += 1

        if self.num_bad_epochs >= self.patience:
            print(f"Early stop at {runner.epoch} epoch")
            runner.need_early_stop = True

catalyst/core/callbacks/early_stop.py
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9fcbefaefb..1f6967e843 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -31,6 +31,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
 
 - `.dependabot/config.yml` contained invalid details ([#781](https://github.com/catalyst-team/catalyst/issues/781))
 - `LanguageModelingDataset` ([#841](https://github.com/catalyst-team/catalyst/pull/841))
+- EarlyStoppingCallback considers first epoch as bad
+ ([#854](https://github.com/catalyst-team/catalyst/issues/854))
+
 
 ## [20.06] - 2020-06-04
 
diff --git a/catalyst/core/callbacks/early_stop.py b/catalyst/core/callbacks/early_stop.py
index de2dce2547..bf66366021 100644
--- a/catalyst/core/callbacks/early_stop.py
+++ b/catalyst/core/callbacks/early_stop.py
@@ -51,9 +51,7 @@ def on_epoch_end(self, runner: IRunner) -> None:
             return
 
         score = runner.valid_metrics[self.metric]
-        if self.best_score is None:
-            self.best_score = score
-        if self.is_better(score, self.best_score):
+        if self.best_score is None or self.is_better(score, self.best_score):
             self.num_bad_epochs = 0
             self.best_score = score
         else:
diff --git a/catalyst/core/callbacks/tests/__init__.py b/catalyst/core/callbacks/tests/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/catalyst/core/callbacks/tests/test_early_stop.py b/catalyst/core/callbacks/tests/test_early_stop.py
new file mode 100644
index 0000000000..74f0e607c1
--- /dev/null
+++ b/catalyst/core/callbacks/tests/test_early_stop.py
@@ -0,0 +1,17 @@
+from unittest.mock import MagicMock, PropertyMock
+
+from catalyst.core import EarlyStoppingCallback
+
+
+def test_patience1():
+    """@TODO: Docs. Contribution is welcome."""
+    early_stop = EarlyStoppingCallback(1)
+    runner = MagicMock()
+    type(runner).stage_name = PropertyMock(return_value="training")
+    type(runner).valid_metrics = PropertyMock(return_value={"loss": 0.001})
+    stop_mock = PropertyMock(return_value=False)
+    type(runner).need_early_stop = stop_mock
+
+    early_stop.on_epoch_end(runner)
+
+    assert stop_mock.mock_calls == []

EarlyStoppingCallback considers first epoch as bad
## 🐛 Bug Report
EarlyStoppingCallback considers first epoch as bad. This can lead for example to always stopping after first epoch if patience=1.


### How To Reproduce
You can train a model with early stopping and patience=1 and see that it always stops after first epoch. Or you can use the unit test below that I added to pull request.

#### Code sample
```python
from unittest.mock import MagicMock, PropertyMock

from catalyst.core import EarlyStoppingCallback


def test_patience1():
    """@TODO: Docs. Contribution is welcome."""
    early_stop = EarlyStoppingCallback(1)
    runner = MagicMock()
    type(runner).stage_name = PropertyMock(return_value="training")
    type(runner).valid_metrics = PropertyMock(return_value={"loss": 0.001})
    stop_mock = PropertyMock(return_value=False)
    type(runner).need_early_stop = stop_mock

    early_stop.on_epoch_end(runner)

    assert stop_mock.mock_calls == []
```

### Expected behavior
Training doesn't stop after first epoch. And the unit test passes.


### Environment
```bash
Catalyst version: 20.06
PyTorch version: 1.5.1
Is debug build: No
CUDA used to build PyTorch: None
TensorFlow version: N/A
TensorBoard version: 2.2.2

OS: Mac OSX 10.15.5
GCC version: Could not collect
CMake version: version 3.8.0

Python version: 3.7
Is CUDA available: No
CUDA runtime version: No CUDA
GPU models and configuration: No CUDA
Nvidia driver version: No CUDA
cuDNN version: No CUDA

Versions of relevant libraries:
[pip3] catalyst-codestyle==20.4
[pip3] catalyst-sphinx-theme==1.1.1
[pip3] efficientnet-pytorch==0.6.3
[pip3] numpy==1.18.5
[pip3] segmentation-models-pytorch==0.1.0
[pip3] tensorboard==2.2.2
[pip3] tensorboard-plugin-wit==1.6.0.post3
[pip3] tensorboardX==2.0
[pip3] torch==1.5.1
[pip3] torchvision==0.6.1
[conda] catalyst-codestyle        20.4                      <pip>
[conda] catalyst-sphinx-theme     1.1.1                     <pip>
[conda] efficientnet-pytorch      0.6.3                     <pip>
[conda] numpy                     1.18.5                    <pip>
[conda] segmentation-models-pytorch 0.1.0                     <pip>
[conda] tensorboard               2.2.2                     <pip>
[conda] tensorboard-plugin-wit    1.6.0.post3               <pip>
[conda] tensorboardX              2.0                       <pip>
[conda] torch                     1.5.1                     <pip>
[conda] torchvision               0.6.1                     <pip>
```

