const assert = require('node:assert/strict');
const ObjectID = require('bson-objectid').default;
const Mention = require('../../../../../core/server/services/mentions/mention');
const MentionsAPI = require('../../../../../core/server/services/mentions/mentions-api');
const InMemoryMentionRepository = require('../../../../../core/server/services/mentions/in-memory-mention-repository');
const sinon = require('sinon');
const cheerio = require('cheerio');

const mockRoutingService = {
    async pageExists() {
        return true;
    }
};
const mockResourceService = {
    async getByURL() {
        return {
            type: null,
            id: null
        };
    }
};

const mockWebmentionMetadata = {
    async fetch() {
        return {
            siteTitle: 'Clickbait News',
            title: 'This egg breakfast will make you cry',
            excerpt: 'How many times have you woken up and almost cancelled your church plans? Well this breakfast is about to change everything, a hearty, faith restoring egg dish that will get your tastebuds in a twist.',
            author: 'Dr Egg Man',
            image: new URL('https://unsplash.com/photos/QAND9huzD04'),
            favicon: new URL('https://ghost.org/favicon.ico'),
            body: `<html><body><p>Some HTML and a <a href='https://target.com/'>mentioned url</a></p></body></html>`,
            contentType: 'text/html'
        };
    }
};

function addMinutes(date, minutes) {
    date.setMinutes(date.getMinutes() + minutes);

    return date;
}

describe('MentionsAPI', function () {
    beforeEach(function () {
        sinon.restore();
    });

    it('Can generate a mentions report', async function () {
        // this.retries(1);
        const repository = new InMemoryMentionRepository();
        const api = new MentionsAPI({
            repository,
            routingService: mockRoutingService,
            resourceService: mockResourceService,
            webmentionMetadata: mockWebmentionMetadata
        });

        const mention = await api.processWebmention({
            source: new URL('https://source.com'),
            target: new URL('https://target.com'),
            payload: {}
        });

        assert(mention instanceof Mention);

        const now = new Date();

        const report = await api.getMentionReport(new Date(0), now);

        assert.deepEqual(report.startDate, new Date(0));
        assert.deepEqual(report.endDate, now);
    });

    it('Can list paginated mentions', async function () {
        const repository = new InMemoryMentionRepository();
        const api = new MentionsAPI({
            repository,
            routingService: mockRoutingService,
            resourceService: mockResourceService,
            webmentionMetadata: mockWebmentionMetadata
        });

        const mention = await api.processWebmention({
            source: new URL('https://source.com'),
            target: new URL('https://target.com'),
            payload: {}
        });

        assert(mention instanceof Mention);

        const page = await api.listMentions({
            limit: 1,
            page: 1
        });

        assert.equal(page.data[0].id, mention.id);
    });

    it('Can list all mentions', async function () {
        const repository = new InMemoryMentionRepository();
        const api = new MentionsAPI({
            repository,
            routingService: mockRoutingService,
            resourceService: mockResourceService,
            webmentionMetadata: mockWebmentionMetadata
        });

        const mention = await api.processWebmention({
            source: new URL('https://source.com'),
            target: new URL('https://target.com'),
            payload: {}
        });

        assert(mention instanceof Mention);

        const page = await api.listMentions({
            limit: 'all'
        });

        assert.equal(page.data[0].id, mention.id);
    });

    it('Can list filtered mentions', async function () {
        const repository = new InMemoryMentionRepository();
        const api = new MentionsAPI({
            repository,
            routingService: mockRoutingService,
            resourceService: mockResourceService,
            webmentionMetadata: mockWebmentionMetadata
        });

        const mentionOne = await api.processWebmention({
            source: new URL('https://diff-source.com'),
            target: new URL('https://target.com'),
            payload: {}
        });
        const mentionTwo = await api.processWebmention({
            source: new URL('https://source.com'),
            target: new URL('https://target.com'),
            payload: {}
        });

        assert(mentionOne instanceof Mention);
        assert(mentionTwo instanceof Mention);

        const page = await api.listMentions({
            filter: 'source.host:source.com',
            limit: 'all'
        });

        assert(page.meta.pagination.total === 1);
        assert(page.data[0].id === mentionTwo.id);
    });

    it('Can list mentions in descending order', async function () {
        const repository = new InMemoryMentionRepository();
        const api = new MentionsAPI({
            repository,
            routingService: mockRoutingService,
            resourceService: mockResourceService,
            webmentionMetadata: mockWebmentionMetadata
        });

        const mentionOne = await api.processWebmention({
            source: new URL('https://source.com'),
            target: new URL('https://target.com'),
            payload: {}
        });

        sinon.useFakeTimers(addMinutes(new Date(), 10).getTime());

        const mentionTwo = await api.processWebmention({
            source: new URL('https://source2.com'),
            target: new URL('https://target.com'),
            payload: {}
        });

        assert(mentionOne instanceof Mention);
        assert(mentionTwo instanceof Mention);

        const page = await api.listMentions({
            limit: 'all',
            order: 'created_at desc'
        });

        assert(page.meta.pagination.total === 2);
        assert(page.data[0].id === mentionTwo.id, 'First mention should be the second one in descending order');
        assert(page.data[1].id === mentionOne.id, 'Second mention should be the first one in descending order');
    });

    it('Can list mentions in ascending order', async function () {
        const repository = new InMemoryMentionRepository();
        const api = new MentionsAPI({
            repository,
            routingService: mockRoutingService,
            resourceService: mockResourceService,
            webmentionMetadata: mockWebmentionMetadata
        });

        const mentionOne = await api.processWebmention({
            source: new URL('https://source.com'),
            target: new URL('https://target.com'),
            payload: {}
        });

        sinon.useFakeTimers(addMinutes(new Date(), 10).getTime());

        const mentionTwo = await api.processWebmention({
            source: new URL('https://source2.com'),
            target: new URL('https://target.com'),
            payload: {}
        });

        assert(mentionOne instanceof Mention);
        assert(mentionTwo instanceof Mention);

        const page = await api.listMentions({
            limit: 'all',
            order: 'created_at asc'
        });

        assert(page.meta.pagination.total === 2);
        assert(page.data[0].id === mentionOne.id, 'First mention should be the first one in ascending order');
        assert(page.data[1].id === mentionTwo.id, 'Second mention should be the second one in ascending order');
    });

    it('Can update recommendations', async function () {
        const repository = new InMemoryMentionRepository();
        const fetchStub = sinon.stub();
        // First two calls succeed (processWebmention for source1 and source2)
        fetchStub.onCall(0).resolves(mockWebmentionMetadata.fetch());
        fetchStub.onCall(1).resolves(mockWebmentionMetadata.fetch());
        // refreshMentions: source1 succeeds, source2 fails 3 times in a row
        fetchStub.onCall(2).resolves(mockWebmentionMetadata.fetch()); // refresh source1 pass 1
        fetchStub.onCall(3).rejects(new Error('fail')); // refresh source2 fail 1
        fetchStub.onCall(4).resolves(mockWebmentionMetadata.fetch()); // refresh source1 pass 2
        fetchStub.onCall(5).rejects(new Error('fail')); // refresh source2 fail 2
        fetchStub.onCall(6).resolves(mockWebmentionMetadata.fetch()); // refresh source1 pass 3
        fetchStub.onCall(7).rejects(new Error('fail')); // refresh source2 fail 3 → deleted

        const api = new MentionsAPI({
            repository,
            routingService: mockRoutingService,
            resourceService: mockResourceService,
            webmentionMetadata: {fetch: fetchStub}
        });

        await api.processWebmention({
            source: new URL('https://source.com'),
            target: new URL('https://target.com'),
            payload: {}
        });

        sinon.useFakeTimers(addMinutes(new Date(), 10).getTime());

        await api.processWebmention({
            source: new URL('https://source2.com'),
            target: new URL('https://target.com'),
            payload: {}
        });

        let page = await api.listMentions({
            limit: 'all'
        });
        assert(page.meta.pagination.total === 2);

        // First refresh: source2 gets failure 1 of 3 — not deleted yet
        await api.refreshMentions({limit: 'all'});
        page = await api.listMentions({limit: 'all'});
        assert.equal(page.meta.pagination.total, 2, 'Mention should survive first hard failure');

        // Second refresh: source2 gets failure 2 of 3 — still not deleted
        await api.refreshMentions({limit: 'all'});
        page = await api.listMentions({limit: 'all'});
        assert.equal(page.meta.pagination.total, 2, 'Mention should survive second hard failure');

        // Third refresh: source2 gets failure 3 of 3 — now deleted
        await api.refreshMentions({limit: 'all'});
        page = await api.listMentions({limit: 'all'});
        assert.equal(page.meta.pagination.total, 1, 'Mention should be deleted after 3rd consecutive hard failure');
    });

    it('Does not delete mention on transient error during refresh', async function () {
        const repository = new InMemoryMentionRepository();
        const transientErr = new Error('Too Many Requests');
        transientErr.transient = true;
        transientErr.statusCode = 429;

        const api = new MentionsAPI({
            repository,
            routingService: mockRoutingService,
            resourceService: mockResourceService,
            webmentionMetadata: {
                fetch: sinon.stub()
                    .onFirstCall().resolves(mockWebmentionMetadata.fetch())
                    .onSecondCall().rejects(transientErr)
            }
        });

        await api.processWebmention({
            source: new URL('https://source.com'),
            target: new URL('https://target.com'),
            payload: {}
        });

        await api.refreshMentions({limit: 'all'});

        const page = await api.listMentions({limit: 'all'});
        assert.equal(page.meta.pagination.total, 1, 'Mention should not be deleted on transient error');
    });

    it('Increments hard failure count but does not delete when count < 3', async function () {
        const repository = new InMemoryMentionRepository();
        const api = new MentionsAPI({
            repository,
            routingService: mockRoutingService,
            resourceService: mockResourceService,
            webmentionMetadata: {
                fetch: sinon.stub()
                    .onFirstCall().resolves(mockWebmentionMetadata.fetch())
                    .onSecondCall().rejects(new Error('404'))
            }
        });

        await api.processWebmention({
            source: new URL('https://source.com'),
            target: new URL('https://target.com'),
            payload: {}
        });

        await api.refreshMentions({limit: 'all'});

        const page = await api.listMentions({limit: 'all'});
        assert.equal(page.meta.pagination.total, 1, 'Mention should survive a single hard failure');
    });

    it('Successful fetch after failures resets the failure count', async function () {
        const repository = new InMemoryMentionRepository();
        const fetchStub = sinon.stub();
        fetchStub.onCall(0).resolves(mockWebmentionMetadata.fetch()); // processWebmention
        fetchStub.onCall(1).rejects(new Error('fail')); // refresh 1: hard failure 1
        fetchStub.onCall(2).rejects(new Error('fail')); // refresh 2: hard failure 2
        fetchStub.onCall(3).resolves(mockWebmentionMetadata.fetch()); // refresh 3: success → resets count
        fetchStub.onCall(4).rejects(new Error('fail')); // refresh 4: hard failure 1 again
        fetchStub.onCall(5).rejects(new Error('fail')); // refresh 5: hard failure 2

        const api = new MentionsAPI({
            repository,
            routingService: mockRoutingService,
            resourceService: mockResourceService,
            webmentionMetadata: {fetch: fetchStub}
        });

        await api.processWebmention({
            source: new URL('https://source.com'),
            target: new URL('https://target.com'),
            payload: {}
        });

        // Two hard failures
        await api.refreshMentions({limit: 'all'});
        await api.refreshMentions({limit: 'all'});

        // One success — should reset count
        await api.refreshMentions({limit: 'all'});

        // Two more hard failures — count should be 2, not 4
        await api.refreshMentions({limit: 'all'});
        await api.refreshMentions({limit: 'all'});

        const page = await api.listMentions({limit: 'all'});
        assert.equal(page.meta.pagination.total, 1, 'Mention should survive because success reset the failure count');
    });

    it('Can handle updating mentions', async function () {
        const repository = new InMemoryMentionRepository();
        const api = new MentionsAPI({
            repository,
            routingService: mockRoutingService,
            resourceService: mockResourceService,
            webmentionMetadata: mockWebmentionMetadata
        });

        const mentionOne = await api.processWebmention({
            source: new URL('https://source.com'),
            target: new URL('https://target.com'),
            payload: {}
        });

        const mentionTwo = await api.processWebmention({
            source: new URL('https://source.com'),
            target: new URL('https://target.com'),
            payload: {
                new: 'info'
            }
        });

        assert(mentionOne.id === mentionTwo.id);

        const page = await api.listMentions({
            limit: 'all'
        });

        assert(page.meta.pagination.total === 1);
        assert(page.data[0].id === mentionOne.id);
    });

    it('Will error if the target page does not exist', async function () {
        const repository = new InMemoryMentionRepository();
        const api = new MentionsAPI({
            repository,
            routingService: {
                async pageExists() {
                    return false;
                }
            },
            resourceService: mockResourceService,
            webmentionMetadata: mockWebmentionMetadata
        });

        let errored = false;
        try {
            await api.processWebmention({
                source: new URL('https://source.com'),
                target: new URL('https://target.com'),
                payload: {}
            });
        } catch (err) {
            errored = true;
        } finally {
            assert(errored);
        }
    });

    it('Handles verify errors', async function () {
        const repository = new InMemoryMentionRepository();
        sinon.stub(cheerio, 'load').throws(new Error('Test error'));

        const api = new MentionsAPI({
            repository,
            routingService: {
                async pageExists() {
                    return true;
                }
            },
            resourceService: mockResourceService,
            webmentionMetadata: mockWebmentionMetadata
        });

        await api.processWebmention({
            source: new URL('https://source.com'),
            target: new URL('https://target.com'),
            payload: {}
        });
    });

    it('Will only store resource if if the resource type is post', async function () {
        const repository = new InMemoryMentionRepository();
        const api = new MentionsAPI({
            repository,
            routingService: mockRoutingService,
            resourceService: {
                async getByURL() {
                    return {
                        type: 'post',
                        id: new ObjectID
                    };
                }
            },
            webmentionMetadata: mockWebmentionMetadata
        });

        const mention = await api.processWebmention({
            source: new URL('https://source.com'),
            target: new URL('https://target.com'),
            payload: {}
        });

        assert(mention instanceof Mention);

        const page = await api.listMentions({
            limit: 'all'
        });

        assert.equal(page.data[0].id, mention.id);
    });

    it('Will delete an existing mention if the target page does not exist', async function () {
        const repository = new InMemoryMentionRepository();
        const api = new MentionsAPI({
            repository,
            routingService: {
                pageExists: sinon.stub().onFirstCall().resolves(true).onSecondCall().resolves(false)
            },
            resourceService: {
                async getByURL() {
                    return {
                        type: 'post',
                        id: new ObjectID
                    };
                }
            },
            webmentionMetadata: mockWebmentionMetadata
        });

        checkFirstMention: {
            const mention = await api.processWebmention({
                source: new URL('https://source.com'),
                target: new URL('https://target.com'),
                payload: {}
            });

            const page = await api.listMentions({
                limit: 'all'
            });

            assert.equal(page.data[0].id, mention.id);
            break checkFirstMention;
        }

        checkMentionDeleted: {
            await api.processWebmention({
                source: new URL('https://source.com'),
                target: new URL('https://target.com'),
                payload: {}
            });

            const page = await api.listMentions({
                limit: 'all'
            });

            assert.equal(page.data.length, 0);
            break checkMentionDeleted;
        }
    });

    it('Will delete and restore an existing mention if the source page does not exist', async function () {
        const repository = new InMemoryMentionRepository();
        const api = new MentionsAPI({
            repository,
            routingService: mockRoutingService,
            resourceService: {
                async getByURL() {
                    return {
                        type: 'post',
                        id: new ObjectID
                    };
                }
            },
            webmentionMetadata: {
                fetch: sinon.stub()
                    .onFirstCall().resolves(mockWebmentionMetadata.fetch())
                    .onSecondCall().rejects(new Error('fail'))
                    .onThirdCall().rejects(new Error('fail'))
                    .onCall(3).rejects(new Error('fail'))
                    .onCall(4).resolves(mockWebmentionMetadata.fetch())
            }
        });

        checkFirstMention: {
            const mention = await api.processWebmention({
                source: new URL('https://source.com'),
                target: new URL('https://target.com'),
                payload: {}
            });

            const page = await api.listMentions({
                limit: 'all'
            });

            assert.equal(page.data[0].id, mention.id);
            break checkFirstMention;
        }

        checkMentionDeleted: {
            // Need 3 consecutive hard failures to delete
            await api.processWebmention({
                source: new URL('https://source.com'),
                target: new URL('https://target.com'),
                payload: {}
            });
            await api.processWebmention({
                source: new URL('https://source.com'),
                target: new URL('https://target.com'),
                payload: {}
            });
            await api.processWebmention({
                source: new URL('https://source.com'),
                target: new URL('https://target.com'),
                payload: {}
            });

            const page = await api.listMentions({
                limit: 'all'
            });

            assert.equal(page.data.length, 0);
            break checkMentionDeleted;
        }

        checkRestored: {
            const mention = await api.processWebmention({
                source: new URL('https://source.com'),
                target: new URL('https://target.com'),
                payload: {}
            });

            const page = await api.listMentions({
                limit: 'all'
            });

            assert.equal(page.data[0].id, mention.id);
            break checkRestored;
        }
    });

    it('Will delete and restore an existing mention if the target url is not present on the source page', async function () {
        const repository = new InMemoryMentionRepository();
        const api = new MentionsAPI({
            repository,
            routingService: mockRoutingService,
            resourceService: {
                async getByURL() {
                    return {
                        type: 'post',
                        id: new ObjectID
                    };
                }
            },
            webmentionMetadata: {
                fetch: sinon.stub()
                    .onFirstCall().resolves(mockWebmentionMetadata.fetch())
                    .onSecondCall().resolves({...(await mockWebmentionMetadata.fetch()), body: 'test'})
                    .onThirdCall().resolves(mockWebmentionMetadata.fetch())
            }
        });

        checkFirstMention: {
            const mention = await api.processWebmention({
                source: new URL('https://source.com'),
                target: new URL('https://target.com'),
                payload: {}
            });

            const page = await api.listMentions({
                limit: 'all'
            });

            assert.equal(page.data[0].id, mention.id);
            break checkFirstMention;
        }

        checkMentionDeleted: {
            await api.processWebmention({
                source: new URL('https://source.com'),
                target: new URL('https://target.com'),
                payload: {}
            });

            const page = await api.listMentions({
                limit: 'all'
            });

            assert.equal(page.data.length, 0);
            break checkMentionDeleted;
        }

        checkRestored: {
            const mention = await api.processWebmention({
                source: new URL('https://source.com'),
                target: new URL('https://target.com'),
                payload: {}
            });

            const page = await api.listMentions({
                limit: 'all'
            });

            assert.equal(page.data[0].id, mention.id);
            break checkRestored;
        }
    });

    it('Will throw for new mentions if the source page is not found', async function () {
        const repository = new InMemoryMentionRepository();
        const api = new MentionsAPI({
            repository,
            routingService: mockRoutingService,
            resourceService: {
                async getByURL() {
                    return {
                        type: 'post',
                        id: new ObjectID
                    };
                }
            },
            webmentionMetadata: {
                fetch: sinon.stub().rejects(new Error(''))
            }
        });

        let error = null;
        try {
            await api.processWebmention({
                source: new URL('https://source.com'),
                target: new URL('https://target.com'),
                payload: {}
            });
        } catch (err) {
            error = err;
        } finally {
            assert(error);
        }
    });
});
