File size: 26,996 Bytes
5f400f8
 
6c3a31d
5f400f8
6c3a31d
b737d7a
6c3a31d
 
 
5f400f8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6c3a31d
5f400f8
 
 
 
 
 
529b973
5f400f8
6c3a31d
5f400f8
 
 
6c3a31d
5f400f8
 
529b973
5f400f8
529b973
6c3a31d
529b973
 
 
 
 
 
 
 
 
 
 
 
5f400f8
 
 
 
6c3a31d
5f400f8
 
 
 
 
 
 
 
 
6c3a31d
5f400f8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
529b973
 
 
 
 
 
 
 
 
6c3a31d
529b973
 
 
 
 
 
 
6c3a31d
529b973
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6c3a31d
529b973
 
 
 
 
 
d55a0af
529b973
 
 
 
 
 
 
 
 
 
6c3a31d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
import pytest

from bot.db import DEFAULT_CAPTION, DEFAULT_FORMAT, DEFAULT_SETTINGS, MAX_FORMATS, Database
from bot.errors import FormatError
from bot.formats import (
    NAME_MAX, FormatSettings, apply_format, default_auto_format, default_metadata_changes, has_default_metadata,
    has_metadata, other_kind, validate_name,
)
from bot.parser import DEFAULT_TEMPLATE
from bot.settings_ops import apply_setting

U, V = 111, 222


@pytest.fixture
async def db(tmp_path):
    database = Database(f"sqlite+aiosqlite:///{tmp_path}/f.db")
    await database.init()
    yield database
    await database.close()


# ---------------------------------------------------------------------------
# storage
# ---------------------------------------------------------------------------


async def test_a_new_format_has_defaults_and_shows_up_in_the_list(db):
    fmt = await db.create_format(U, "One Piece")
    assert fmt["name"] == "One Piece" and fmt["id"] > 0 and fmt["has_thumb"] is False
    assert {k: fmt[k] for k in DEFAULT_FORMAT} == DEFAULT_FORMAT
    await db.create_format(U, "Dragon Ball", template="DB - E{episode}")
    listed = await db.list_formats(U)
    assert [f["name"] for f in listed] == ["One Piece", "Dragon Ball"]  # creation order
    assert listed[1]["template"] == "DB - E{episode}"
    assert await db.count_formats(U) == 2 and await db.count_formats(V) == 0
    assert (await db.get_format(U, fmt["id"]))["name"] == "One Piece"


async def test_names_are_unique_per_user_ignoring_case(db):
    await db.create_format(U, "One Piece")
    with pytest.raises(FormatError) as exc:
        await db.create_format(U, "one piece")
    assert exc.value.code == "taken"
    await db.create_format(V, "One Piece")  # someone else may use the same name


async def test_a_user_can_save_twelve_formats_and_no_more(db):
    for i in range(MAX_FORMATS):
        await db.create_format(U, f"Format {i}")
    with pytest.raises(FormatError) as exc:
        await db.create_format(U, "One too many")
    assert exc.value.code == "limit"
    first = (await db.list_formats(U))[0]
    assert await db.delete_format(U, first["id"])
    await db.create_format(U, "One too many")  # a slot is free again
    assert await db.count_formats(U) == MAX_FORMATS


async def test_update_changes_fields_and_merges_meta(db):
    fmt = await db.create_format(U, "One Piece")
    out = await db.update_format(U, fmt["id"], template="One Piece - E{episode}", keep_audio=["japanese", "english"])
    assert out["template"] == "One Piece - E{episode}" and out["keep_audio"] == ["japanese", "english"]
    await db.update_format(U, fmt["id"], meta={"title": "One Piece"})
    out = await db.update_format(U, fmt["id"], meta={"author": "@Me"}, keep_subtitle=["english"])
    assert out["meta"]["title"] == "One Piece" and out["meta"]["author"] == "@Me" and out["meta"]["artist"] == ""
    assert out["keep_subtitle"] == ["english"]
    with pytest.raises(KeyError):
        await db.update_format(U, fmt["id"], bogus=1)


async def test_formats_persist_across_database_instances(db):
    fmt = await db.create_format(U, "One Piece", template="X {title}")
    await db.update_format(U, fmt["id"], meta={"video": "V"}, keep_audio=["2", "1"])
    await db.set_format_thumb(U, fmt["id"], b"jpegbytes")
    other = Database(db.url)
    got = await other.get_format(U, fmt["id"])
    assert got["template"] == "X {title}" and got["meta"]["video"] == "V" and got["keep_audio"] == ["2", "1"] and got["has_thumb"]
    assert await other.get_format_thumb(U, fmt["id"]) == b"jpegbytes"
    await other.close()


async def test_renaming_a_format_checks_the_other_names_only(db):
    a = await db.create_format(U, "One Piece")
    await db.create_format(U, "Dragon Ball")
    assert (await db.update_format(U, a["id"], name="ONE PIECE"))["name"] == "ONE PIECE"  # its own name, new case
    with pytest.raises(FormatError):
        await db.update_format(U, a["id"], name="dragon ball")
    assert (await db.get_format(U, a["id"]))["name"] == "ONE PIECE"


async def test_thumbnails_belong_to_one_format(db):
    a = await db.create_format(U, "A")
    b = await db.create_format(U, "B")
    assert await db.get_format_thumb(U, a["id"]) is None
    assert await db.set_format_thumb(U, a["id"], b"A-thumb")
    assert [f["has_thumb"] for f in await db.list_formats(U)] == [True, False]
    assert await db.get_format_thumb(U, b["id"]) is None
    assert await db.set_format_thumb(U, a["id"], None)
    assert (await db.get_format(U, a["id"]))["has_thumb"] is False


async def test_nobody_can_touch_another_users_format(db):
    fmt = await db.create_format(U, "Mine")
    assert await db.get_format(V, fmt["id"]) is None
    assert await db.update_format(V, fmt["id"], template="x") is None
    assert await db.set_format_thumb(V, fmt["id"], b"x") is False
    assert await db.get_format_thumb(V, fmt["id"]) is None
    assert await db.delete_format(V, fmt["id"]) is False
    assert (await db.get_format(U, fmt["id"]))["name"] == "Mine"


async def test_delete_and_missing_ids(db):
    fmt = await db.create_format(U, "Gone")
    assert await db.delete_format(U, fmt["id"]) is True
    assert await db.delete_format(U, fmt["id"]) is False
    assert await db.get_format(U, fmt["id"]) is None and await db.update_format(U, fmt["id"], template="x") is None


# ---------------------------------------------------------------------------
# helpers
# ---------------------------------------------------------------------------


def test_validate_name():
    assert validate_name("  One   Piece  ") == (True, "One Piece")
    assert validate_name("")[0] is False and validate_name("   ")[0] is False and validate_name(None)[0] is False
    ok, msg = validate_name("x" * (NAME_MAX + 1))
    assert not ok and str(NAME_MAX) in msg
    assert validate_name("x" * NAME_MAX)[0] is True
    assert validate_name("one\npiece") == (True, "one piece")


def fmt_with(**kw):
    base = {"id": 1, "name": "F", "kind": "manual", "has_thumb": False, "template": "T {title}", "caption": DEFAULT_FORMAT["caption"],
            "meta": dict(DEFAULT_FORMAT["meta"]), "keep_audio": [], "keep_subtitle": []}
    base.update(kw)
    return base


def test_apply_format_lays_the_format_over_the_settings_without_mutating_them():
    settings = dict(DEFAULT_SETTINGS, mode="caption", caption="<i>{original}</i>", file_type="document", remove_words=["x"],
                    target_channel=-100123, metadata_on=False, keep_audio=["french"])
    settings["meta"] = dict(DEFAULT_SETTINGS["meta"], title="DEFAULT TITLE")
    fmt = fmt_with(template="One Piece - E{episode}", meta=dict(DEFAULT_FORMAT["meta"], author="@Me"), keep_subtitle=["english"])
    out = apply_format(settings, fmt)
    assert out["template"] == "One Piece - E{episode}"
    assert out["meta"]["author"] == "@Me" and out["meta"]["title"] == ""  # the format's own metadata, not the default metadata
    assert out["keep_audio"] == [] and out["keep_subtitle"] == ["english"]
    assert out["metadata_on"] is True
    assert out["caption"] == "<b>{filename}</b>"  # the format's own, not the user's <i>{original}</i>
    # everything else is still the user's
    assert (out["mode"], out["file_type"], out["remove_words"], out["target_channel"]) == ("caption", "document", ["x"], -100123)
    assert "template" not in settings and settings["meta"]["title"] == "DEFAULT TITLE"
    assert settings["caption"] == "<i>{original}</i>"


def test_a_format_brings_its_own_caption_and_never_borrows_the_users():
    mine = dict(DEFAULT_SETTINGS, caption="<i>{original}</i> by me")
    assert apply_format(mine, fmt_with(caption="<b>{title}</b> · {size}"))["caption"] == "<b>{title}</b> · {size}"
    assert apply_format(mine, fmt_with())["caption"] == "<b>{filename}</b>"  # none of its own: the new file name in bold
    odd = fmt_with()
    del odd["caption"]  # a snapshot without the field
    assert apply_format(mine, odd)["caption"] == "<b>{filename}</b>"
    assert apply_format(mine, fmt_with(caption=""))["caption"] == "<b>{filename}</b>"
    assert mine["caption"] == "<i>{original}</i> by me"


def test_a_format_without_metadata_leaves_the_file_alone():
    assert not has_metadata(fmt_with())
    settings = dict(DEFAULT_SETTINGS, metadata_on=True)  # default metadata on, but the format sets nothing
    assert apply_format(settings, fmt_with())["metadata_on"] is False
    assert has_metadata(fmt_with(keep_audio=["1"])) and has_metadata(fmt_with(meta=dict(DEFAULT_FORMAT["meta"], video="v")))


# ---------------------------------------------------------------------------
# editing a format through the shared validation
# ---------------------------------------------------------------------------


async def test_settings_ops_edit_a_format_with_the_same_rules_as_the_users_own_settings(db):
    fmt = await db.create_format(U, "One Piece")
    store = FormatSettings(db, fmt["id"])

    ok, msg = await apply_setting(store, U, "template", "One Piece - E{episode}")
    assert ok and "Template saved" in msg and "One Piece - E" in msg
    ok, msg = await apply_setting(store, U, "mt_title", "One Piece")
    assert ok and "Metadata is Off" not in msg
    ok, _ = await apply_setting(store, U, "mt_audio", "{lang} Audio")
    assert ok
    ok, msg = await apply_setting(store, U, "keep_audio", "Japanese English")
    assert ok and "turned it On" not in msg
    ok, _ = await apply_setting(store, U, "keep_subtitle", "2 1")
    assert ok

    got = await db.get_format(U, fmt["id"])
    assert got["template"] == "One Piece - E{episode}"
    assert got["meta"]["title"] == "One Piece" and got["meta"]["audio"] == "{lang} Audio"
    assert got["keep_audio"] == ["japanese", "english"] and got["keep_subtitle"] == ["2", "1"]
    # the user's own settings were not touched
    assert await db.get_settings(U) == DEFAULT_SETTINGS

    ok, msg = await apply_setting(store, U, "keep_audio", "off")
    assert ok and "cleared" in msg and (await db.get_format(U, fmt["id"]))["keep_audio"] == []
    ok, msg = await apply_setting(store, U, "mt_title", "off")
    assert ok and (await db.get_format(U, fmt["id"]))["meta"]["title"] == ""
    ok, msg = await apply_setting(store, U, "keep_audio", "bad!word")
    assert not ok and "❌" in msg


async def test_editing_a_deleted_format_raises_lookup_error(db):
    fmt = await db.create_format(U, "Gone")
    store = FormatSettings(db, fmt["id"])
    await db.delete_format(U, fmt["id"])
    with pytest.raises(LookupError):
        await apply_setting(store, U, "template", "x {title}")
    with pytest.raises(LookupError):
        await store.get_settings(U)


# ---------------------------------------------------------------------------
# a format's own caption
# ---------------------------------------------------------------------------


async def test_a_format_starts_with_the_file_name_in_bold_and_keeps_a_caption_of_its_own(db):
    fmt = await db.create_format(U, "One Piece")
    assert fmt["caption"] == "<b>{filename}</b>" == DEFAULT_CAPTION
    out = await db.update_format(U, fmt["id"], caption="<i>{filename}</i> · {size}")
    assert out["caption"] == "<i>{filename}</i> · {size}"
    await db.update_format(U, fmt["id"], template="X {title}", meta={"title": "T"})  # other edits leave it alone
    other = Database(db.url)
    assert (await other.get_format(U, fmt["id"]))["caption"] == "<i>{filename}</i> · {size}"
    await other.close()
    assert (await db.list_formats(U))[0]["caption"] == "<i>{filename}</i> · {size}"
    assert await db.get_settings(U) == DEFAULT_SETTINGS  # formats never touch the user's own settings


async def test_formats_saved_before_captions_existed_get_the_default_one(db):
    import json

    from bot.db import Format

    async with db._sessions() as session:
        session.add(Format(user_id=U, name="Old", data=json.dumps({"template": "Old - {title}", "keep_audio": ["english"]})))
        session.add(Format(user_id=U, name="Broken", data=json.dumps({"caption": 7})))
        session.add(Format(user_id=U, name="Blank", data=json.dumps({"caption": "   "})))
        await session.commit()
    old, broken, blank = await db.list_formats(U)
    assert old["template"] == "Old - {title}" and old["keep_audio"] == ["english"] and old["caption"] == "<b>{filename}</b>"
    assert broken["caption"] == "<b>{filename}</b>" and blank["caption"] == "<b>{filename}</b>"


async def test_a_copy_of_a_format_has_its_caption(db):
    fmt = await db.create_format(U, "One Piece")
    await db.update_format(U, fmt["id"], caption="<code>{filename}</code>")
    copy = await db.copy_format(V, U, fmt["id"])
    assert copy["caption"] == "<code>{filename}</code>"
    await db.update_format(V, copy["id"], caption="<b>{title}</b>")
    assert (await db.get_format(U, fmt["id"]))["caption"] == "<code>{filename}</code>"


async def test_settings_ops_edit_a_formats_caption_with_the_checks_of_the_auto_format_caption(db):
    fmt = await db.create_format(U, "One Piece")
    store = FormatSettings(db, fmt["id"])
    ok, msg = await apply_setting(store, U, "caption", "<b>{filename}</b> · {size} · {duration}")
    assert ok and "Caption template saved" in msg and "Unknown" not in msg
    assert (await db.get_format(U, fmt["id"]))["caption"] == "<b>{filename}</b> · {size} · {duration}"
    ok, msg = await apply_setting(store, U, "caption", "<i>{nonsense}</i>")
    assert ok and "Unknown variables" in msg and "{nonsense}" in msg  # kept as plain text, as for the auto format's caption
    ok, msg = await apply_setting(store, U, "caption", "x" * 801)
    assert not ok and "too long" in msg and (await db.get_format(U, fmt["id"]))["caption"] == "<i>{nonsense}</i>"
    ok, msg = await apply_setting(store, U, "caption", "")
    assert not ok and "can't be empty" in msg
    for word in ("reset", "off", "none", "clear"):
        await apply_setting(store, U, "caption", "<u>{filename}</u>")
        ok, _ = await apply_setting(store, U, "caption", word)
        assert ok and (await db.get_format(U, fmt["id"]))["caption"] == "<b>{filename}</b>", word  # the file name in bold again
    assert (await store.get_settings(U))["caption"] == "<b>{filename}</b>"
    assert await db.get_settings(U) == DEFAULT_SETTINGS


# ---------------------------------------------------------------------------
# two kinds: 12 auto formats and 12 manual ones
# ---------------------------------------------------------------------------


async def test_a_format_is_manual_unless_it_is_made_as_an_auto_one(db):
    assert (await db.create_format(U, "A"))["kind"] == "manual"
    assert (await db.create_format(U, "B", kind="manual"))["kind"] == "manual"
    auto = await db.create_format(U, "C", kind="auto")
    assert auto["kind"] == "auto" and (await db.get_format(U, auto["id"]))["kind"] == "auto"
    with pytest.raises(KeyError):
        await db.create_format(U, "D", kind="both")
    assert other_kind("auto") == "manual" and other_kind("manual") == "auto"


async def test_each_kind_lists_and_counts_its_own_formats(db):
    a1, m1, a2 = await db.create_format(U, "A1", kind="auto"), await db.create_format(U, "M1"), await db.create_format(U, "A2", kind="auto")
    await db.create_format(V, "Theirs", kind="auto")
    assert [f["id"] for f in await db.list_formats(U)] == [a1["id"], m1["id"], a2["id"]]  # no kind: all of them, oldest first
    assert [f["id"] for f in await db.list_formats(U, "auto")] == [a1["id"], a2["id"]]
    assert [f["id"] for f in await db.list_formats(U, "manual")] == [m1["id"]]
    assert (await db.count_formats(U), await db.count_formats(U, "auto"), await db.count_formats(U, "manual")) == (3, 2, 1)
    assert await db.count_formats(V, "manual") == 0 and await db.count_formats(V, "auto") == 1


async def test_a_user_can_save_twelve_formats_of_each_kind_and_no_more(db):
    for kind in ("auto", "manual"):
        for i in range(MAX_FORMATS):
            await db.create_format(U, f"{kind} {i}", kind=kind)
        with pytest.raises(FormatError) as exc:
            await db.create_format(U, "One too many", kind=kind)
        assert exc.value.code == "limit"
    assert await db.count_formats(U) == 2 * MAX_FORMATS
    first = (await db.list_formats(U, "auto"))[0]
    assert await db.delete_format(U, first["id"])
    await db.create_format(U, "One too many", kind="auto")  # a slot of that kind is free again
    with pytest.raises(FormatError):
        await db.create_format(U, "Still too many", kind="manual")  # the manual ones are still full


async def test_a_name_only_has_to_be_free_within_its_kind(db):
    auto = await db.create_format(U, "One Piece", kind="auto")
    manual = await db.create_format(U, "ONE PIECE")  # the same name, the other kind
    with pytest.raises(FormatError) as exc:
        await db.create_format(U, "one piece", kind="auto")
    assert exc.value.code == "taken"
    other = await db.create_format(U, "Dragon Ball", kind="auto")
    with pytest.raises(FormatError):
        await db.update_format(U, other["id"], name="one piece")  # taken by an auto format
    await db.update_format(U, other["id"], name="Dragon Ball Z")
    await db.update_format(U, manual["id"], name="Dragon Ball Z")  # but free among the manual ones
    assert (await db.get_format(U, auto["id"]))["name"] == "One Piece"


async def test_formats_stored_before_there_were_kinds_are_manual(db):
    import json

    from bot.db import Format

    async with db._sessions() as session:
        session.add(Format(user_id=U, name="Old", data=json.dumps({"template": "Old - {title}"})))
        session.add(Format(user_id=U, name="Odd", data=json.dumps({"kind": "sideways"})))
        session.add(Format(user_id=U, name="Auto", data=json.dumps({"kind": "auto"})))
        await session.commit()
    assert [(f["name"], f["kind"]) for f in await db.list_formats(U)] == [("Old", "manual"), ("Odd", "manual"), ("Auto", "auto")]
    old = (await db.list_formats(U, "manual"))[0]
    out = await db.update_format(U, old["id"], template="New - {title}")  # editing a format keeps its kind
    assert out["kind"] == "manual" and (await db.get_format(U, old["id"]))["template"] == "New - {title}"
    auto = (await db.list_formats(U, "auto"))[0]
    assert (await db.update_format(U, auto["id"], meta={"title": "T"}))["kind"] == "auto"


async def test_copy_format_to_the_other_kind(db):
    src = await db.create_format(U, "One Piece", "E{episode}", kind="auto")
    await db.update_format(U, src["id"], meta={"title": "OP"}, keep_audio=["japanese"], caption="<i>{filename}</i>")
    await db.set_format_thumb(U, src["id"], b"\xff\xd8thumb")

    same = await db.copy_format(V, U, src["id"])  # sharing: the copy keeps the kind
    assert same["kind"] == "auto"
    mine = await db.copy_format(U, U, src["id"], kind="manual")
    assert mine["kind"] == "manual" and mine["name"] == "One Piece" and mine["id"] != src["id"]  # free in the manual names
    assert mine["template"] == "E{episode}" and mine["meta"]["title"] == "OP" and mine["keep_audio"] == ["japanese"]
    assert mine["caption"] == "<i>{filename}</i>" and mine["has_thumb"] and await db.get_format_thumb(U, mine["id"]) == b"\xff\xd8thumb"
    assert [(await db.copy_format(U, U, src["id"], kind="manual"))["name"] for _ in range(2)] == ["One Piece (2)", "One Piece (3)"]
    back = await db.copy_format(U, U, mine["id"], kind="auto")  # and the other way round: "One Piece" is taken among the auto ones
    assert back["kind"] == "auto" and back["name"] == "One Piece (2)"
    await db.update_format(U, mine["id"], template="Changed")  # the copy is independent
    assert (await db.get_format(U, src["id"]))["template"] == "E{episode}"
    with pytest.raises(KeyError):
        await db.copy_format(U, U, src["id"], kind="both")


async def test_copying_to_the_other_kind_stops_at_the_limit_of_that_kind(db):
    src = await db.create_format(U, "One Piece", kind="auto")
    for i in range(MAX_FORMATS):
        await db.create_format(U, f"M{i}")
    with pytest.raises(FormatError) as exc:
        await db.copy_format(U, U, src["id"], kind="manual")
    assert exc.value.code == "limit" and await db.count_formats(U, "manual") == MAX_FORMATS
    assert (await db.copy_format(U, U, src["id"]))["kind"] == "auto"  # the auto ones have room


# ---------------------------------------------------------------------------
# the auto format in use
# ---------------------------------------------------------------------------


async def test_without_auto_formats_there_is_none_in_use(db):
    assert await db.get_auto_format(U) is None
    await db.create_format(U, "Manual only")  # a manual format never renames files by itself
    assert await db.get_auto_format(U) is None and (await db.get_settings(U))["auto_format"] == 0


async def test_the_first_auto_format_is_in_use_and_it_is_remembered(db):
    first = await db.create_format(U, "First", kind="auto")
    await db.create_format(U, "Second", kind="auto")
    assert (await db.get_auto_format(U))["id"] == first["id"]
    assert (await db.get_settings(U))["auto_format"] == first["id"]
    assert (await db.get_auto_format(V)) is None  # per user


async def test_selecting_an_auto_format(db):
    a, b = await db.create_format(U, "A", kind="auto"), await db.create_format(U, "B", kind="auto")
    manual = await db.create_format(U, "M")
    theirs = await db.create_format(V, "Theirs", kind="auto")
    assert (await db.select_auto_format(U, b["id"]))["id"] == b["id"]
    assert (await db.get_auto_format(U))["id"] == b["id"] and (await db.get_settings(U))["auto_format"] == b["id"]
    for fid in (manual["id"], theirs["id"], 99999):  # not an auto format of theirs
        assert await db.select_auto_format(U, fid) is None
    assert (await db.get_auto_format(U))["id"] == b["id"]  # unchanged by the refusals
    await db.select_auto_format(U, a["id"])
    assert (await db.get_auto_format(U))["id"] == a["id"]


async def test_deleting_the_auto_format_in_use_hands_over_to_another(db):
    a = await db.create_format(U, "A", kind="auto")
    b = await db.create_format(U, "B", kind="auto")
    c = await db.create_format(U, "C", kind="auto")
    await db.select_auto_format(U, b["id"])
    await db.delete_format(U, b["id"])
    assert (await db.get_auto_format(U))["id"] == a["id"]  # the oldest left takes over
    assert (await db.get_settings(U))["auto_format"] == a["id"]
    await db.delete_format(U, c["id"])  # deleting one that is not in use changes nothing
    assert (await db.get_auto_format(U))["id"] == a["id"]
    await db.delete_format(U, a["id"])
    assert await db.get_auto_format(U) is None and (await db.get_settings(U))["auto_format"] == 0


# ---------------------------------------------------------------------------
# a format over the user's settings, and the default metadata
# ---------------------------------------------------------------------------


def with_default_metadata(**over):
    settings = dict(DEFAULT_SETTINGS, **over)
    settings["meta"] = dict(DEFAULT_SETTINGS["meta"], title="DEFAULT TITLE", audio="{lang} Audio")
    return settings


def test_an_auto_format_without_metadata_leaves_the_default_metadata_in_charge():
    mine = with_default_metadata(metadata_on=True, keep_audio=["english"])
    out = apply_format(mine, fmt_with(kind="auto"))
    assert out["template"] == "T {title}" and out["metadata_on"] is True  # its switch decides, as it always did
    assert out["meta"]["title"] == "DEFAULT TITLE" and out["keep_audio"] == ["english"]
    off = apply_format(with_default_metadata(metadata_on=False), fmt_with(kind="auto"))
    assert off["metadata_on"] is False and off["meta"]["title"] == "DEFAULT TITLE"  # off: nothing is written


def test_an_auto_format_with_metadata_of_its_own_ignores_the_default_metadata():
    fmt = fmt_with(kind="auto", meta=dict(DEFAULT_FORMAT["meta"], author="@Me"))
    out = apply_format(with_default_metadata(metadata_on=False), fmt)
    assert out["metadata_on"] is True and out["meta"]["author"] == "@Me" and out["meta"]["title"] == "" and out["keep_audio"] == []
    assert apply_format(with_default_metadata(metadata_on=True), fmt)["meta"]["title"] == ""


def test_a_manual_format_never_uses_the_default_metadata():
    out = apply_format(with_default_metadata(metadata_on=True, keep_audio=["english"]), fmt_with(kind="manual"))
    assert out["metadata_on"] is False and out["meta"]["title"] == "" and out["keep_audio"] == []


def test_no_auto_format_means_the_built_in_defaults_and_the_default_metadata():
    built_in = default_auto_format()
    assert built_in["kind"] == "auto" and built_in["template"] == DEFAULT_TEMPLATE and built_in["caption"] == DEFAULT_CAPTION
    assert not built_in["has_thumb"] and not has_metadata(built_in)
    mine = with_default_metadata(metadata_on=True, template="ignored")
    out = apply_format(mine, None)
    assert out["template"] == DEFAULT_TEMPLATE and out["caption"] == DEFAULT_CAPTION
    assert out["metadata_on"] is True and out["meta"]["title"] == "DEFAULT TITLE"
    assert built_in is not default_auto_format() and default_auto_format()["meta"] == DEFAULT_FORMAT["meta"]  # a fresh copy each time


def test_default_metadata_is_what_the_user_set_in_the_metadata_menu():
    assert not has_default_metadata(DEFAULT_SETTINGS)
    assert not has_default_metadata(dict(DEFAULT_SETTINGS, metadata_on=True))  # the switch alone is not metadata
    assert has_default_metadata(with_default_metadata())
    assert has_default_metadata(dict(DEFAULT_SETTINGS, keep_audio=["1"])) and has_default_metadata(dict(DEFAULT_SETTINGS, keep_subtitle=["english"]))
    changes = default_metadata_changes(with_default_metadata(keep_audio=["2", "1"], keep_subtitle=["english"]))
    assert set(changes) == {"meta", "keep_audio", "keep_subtitle"}
    assert changes["meta"]["title"] == "DEFAULT TITLE" and changes["keep_audio"] == ["2", "1"] and changes["keep_subtitle"] == ["english"]
    source = with_default_metadata(keep_audio=["2"])
    changes = default_metadata_changes(source)
    changes["keep_audio"].append("3")
    changes["meta"]["title"] = "other"
    assert source["keep_audio"] == ["2"] and source["meta"]["title"] == "DEFAULT TITLE"  # copies, not the user's own lists


async def test_default_metadata_can_be_copied_into_a_format_of_either_kind(db):
    settings = with_default_metadata(keep_audio=["japanese"], keep_subtitle=["english"])
    for kind in ("auto", "manual"):
        fmt = await db.create_format(U, f"{kind} one", kind=kind)
        await db.update_format(U, fmt["id"], meta={"video": "OLD"}, keep_audio=["french", "2"])
        got = await db.update_format(U, fmt["id"], **default_metadata_changes(settings))
        assert got["meta"]["title"] == "DEFAULT TITLE" and got["meta"]["audio"] == "{lang} Audio"
        assert got["keep_audio"] == ["japanese"] and got["keep_subtitle"] == ["english"] and got["kind"] == kind
        assert got["meta"]["video"] == "" and got["meta"]["author"] == ""  # all six fields are sent: it replaces what the format had